├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── favicon.ico ├── flow-typed └── npm │ └── jest_v22.x.x.js ├── lerna.json ├── package.json ├── packages ├── __tests__ │ └── fixtures │ │ ├── 000-go.ron │ │ ├── 001-conn.ron │ │ ├── 002-hs.ron │ │ ├── 003-calendar-clock.ron │ │ ├── 004-query.ron │ │ ├── 005-push.ron │ │ ├── 006-lwwset.ron │ │ ├── 007-pending.ron │ │ ├── 008-setadd.ron │ │ ├── 009-pending.ron │ │ ├── 010-setrm.ron │ │ ├── 011-react.ron │ │ ├── 012-local-uuids.ron │ │ ├── 013-once.ron │ │ ├── 014-gql-subs.ron │ │ ├── 015-gql-mutation.ron │ │ ├── 016-gql-query.ron │ │ ├── 017-ensure-directive.ron │ │ ├── 018-react-graphql.ron │ │ ├── 019-pending.ron │ │ ├── 020-empty-ack.ron │ │ ├── 021-gql-empty-ack.ron │ │ ├── 022-client-seen.ron │ │ └── index.js ├── api │ ├── .npmignore │ ├── __tests__ │ │ ├── lww.js │ │ ├── new.js │ │ └── set.js │ ├── package.json │ └── src │ │ └── index.js ├── client │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ ├── api.js │ │ ├── init.js │ │ ├── misc.js │ │ ├── mutex.js │ │ └── storage.js │ ├── package.json │ └── src │ │ ├── asyncStorage.js │ │ ├── connection.js │ │ ├── index.js │ │ ├── mutex.js │ │ ├── pending.js │ │ ├── rws.js │ │ └── storage.js ├── clock │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── clock.js │ ├── package.json │ └── src │ │ └── index.js ├── db │ ├── .npmignore │ ├── __tests__ │ │ ├── basic.js │ │ ├── deps.js │ │ ├── partialReactivity.js │ │ └── weak.js │ ├── package.json │ └── src │ │ ├── deps.js │ │ ├── index.js │ │ ├── schema.js │ │ ├── subscription.js │ │ ├── types.js │ │ └── utils.js ├── rdt │ ├── .npmignore │ ├── __tests__ │ │ ├── empty.js │ │ ├── iheap.js │ │ ├── log.js │ │ ├── lww.js │ │ └── set.js │ ├── package.json │ └── src │ │ ├── iheap.js │ │ ├── index.js │ │ ├── log.js │ │ ├── lww.js │ │ └── set.js ├── react │ ├── .babelrc │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── graphql.js.snap │ │ └── graphql.js │ ├── package.json │ └── src │ │ ├── GraphQL.js │ │ ├── Provider.js │ │ └── index.js ├── regular-grammar │ ├── README.md │ ├── index.js │ ├── package.json │ └── test.js ├── ron-grammar │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── index.js │ ├── package.json │ └── src │ │ └── index.js ├── ron-uuid │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ ├── format.js │ │ └── index.js │ ├── package.json │ └── src │ │ └── index.js └── ron │ ├── .npmignore │ ├── README.md │ ├── TODO │ ├── __tests__ │ ├── batch.js │ └── index.js │ ├── package.json │ └── src │ ├── batch.js │ └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { 3 | "loose": true, 4 | "useBuiltIns": false 5 | }]], 6 | "plugins": [ 7 | [ 8 | "transform-object-rest-spread", 9 | { 10 | "useBuiltIns": true 11 | } 12 | ], 13 | "transform-es2015-spread", 14 | "transform-flow-strip-types", 15 | "transform-class-bound-properties", 16 | "transform-node-env-inline" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "plugin:flowtype/recommended" 5 | ], 6 | "plugins": [ 7 | "flowtype" 8 | ], 9 | "settings": { 10 | "flowtype": { 11 | "onlyFilesWithFlowAnnotation": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /lib/.* 3 | /packages/*/lib/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | module.name_mapper='^\(.*\)@swarm\/\(.*\)$' -> '/packages/\2/src' 11 | munge_underscores=true 12 | 13 | [version] 14 | 0.66.0 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build products 2 | dist/* 3 | *.app.js 4 | *.min.js 5 | # byproducts 6 | *.log 7 | *.diff 8 | *.patch 9 | *.cpuprofile 10 | .swarm 11 | .store 12 | *.swp 13 | # dev stuff 14 | .idea 15 | node_modules 16 | coverage 17 | lib 18 | # OS generated files # 19 | ###################### 20 | .DS_Store 21 | .DS_Store? 22 | ._* 23 | .Spotlight-V100 24 | .Trashes 25 | packages/playground 26 | packages/examples 27 | packages/chat 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2018 Victor Grishchenko & friends 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olebedev/swarm/08d816afcea4ba471f4a49460b81cf969d2725c3/favicon.ico -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.5.1", 3 | "packages": [ 4 | "packages/api", 5 | "packages/client", 6 | "packages/clock", 7 | "packages/db", 8 | "packages/rdt", 9 | "packages/react", 10 | "packages/regular-grammar", 11 | "packages/ron-grammar", 12 | "packages/ron-uuid", 13 | "packages/ron" 14 | ], 15 | "npmClient": "yarn", 16 | "useWorkspaces": true, 17 | "commands": { 18 | "publish": { 19 | "message": "chore(release): publish %s" 20 | } 21 | }, 22 | "version": "0.1.2" 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "2.0.0", 4 | "name": "swarm", 5 | "homepage": "http://github.com/gritzko/swarm", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/gritzko/swarm.git" 9 | }, 10 | "author": { 11 | "email": "victor.grishchenko@gmail.com", 12 | "name": "Victor Grishchenko" 13 | }, 14 | "contributors": [ 15 | { 16 | "name": "Aleksei Balandin", 17 | "email": "aleksisha@gmail.com" 18 | }, 19 | { 20 | "name": "Andrey Popp", 21 | "email": "8mayday@gmail.com" 22 | }, 23 | { 24 | "name": "Oleg Lebedev", 25 | "email": "ole6edev@gmail.com" 26 | } 27 | ], 28 | "license": "MIT", 29 | "devDependencies": { 30 | "babel-cli": "^6.26.0", 31 | "babel-core": "^6.26.0", 32 | "babel-eslint": "^8.1.2", 33 | "babel-plugin-transform-class-bound-properties": "^1.0.1", 34 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 35 | "babel-plugin-transform-node-env-inline": "^0.3.0", 36 | "babel-plugin-transform-regenerator": "^6.26.0", 37 | "babel-preset-env": "^1.6.1", 38 | "eslint": "^4.14.0", 39 | "eslint-plugin-flowtype": "^2.40.1", 40 | "flow-bin": "0.66.0", 41 | "flow-copy-source": "^1.2.2", 42 | "flow-coverage-report": "^0.4.1", 43 | "jest": "22.0.3", 44 | "lerna": "^2.5.1", 45 | "rimraf": "^2.6.2" 46 | }, 47 | "workspaces": [ 48 | "packages/api", 49 | "packages/client", 50 | "packages/clock", 51 | "packages/db", 52 | "packages/rdt", 53 | "packages/react", 54 | "packages/regular-grammar", 55 | "packages/ron-grammar", 56 | "packages/ron-uuid", 57 | "packages/ron" 58 | ], 59 | "scripts": { 60 | "test": "jest", 61 | "build": "lerna run build", 62 | "build:clean": "lerna run build:clean", 63 | "build:lib": "lerna run build:lib", 64 | "build:flow": "lerna run build:flow", 65 | "coverage": "flow-coverage-report -o coverage -i 'packages/*/src/**/*.js' -x 'packages/client/src/asyncStorage.js' -t html -t json -t text --threshold 90 && open ./coverage/index.html", 66 | "postinstall": "yarn build" 67 | }, 68 | "jest": { 69 | "testPathIgnorePatterns": [ 70 | "/node_modules/", 71 | "/playground/" 72 | ] 73 | }, 74 | "dependencies": { 75 | "graphql": "0.13.1", 76 | "graphql-anywhere": "4.1.5", 77 | "graphql-tag": "2.8.0", 78 | "invariant": "2.2.2", 79 | "prop-types": "15.6.0", 80 | "react": "16.2.0", 81 | "react-dom": "16.2.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/000-go.ron: -------------------------------------------------------------------------------- 1 | *~ >A 'empty rw handshake, clock sync'? 2 | 'RON database has an arbitrary 60-bit identifier (like #test).' 3 | 'A metadata object (type *db) is non-versioned, immutable key-value dict.' 4 | 'These parameters are global; e.g. any db replica must use the same type of clock.' 5 | 'Replica id granting scheme is also global; replica ids are unique (scoped to db).' 6 | 'Consequently, clocks only maintain their properties in the scope of their db.' 7 | 'Same-db objects/events can reference each other, can be compared.' 8 | *db #test @+ClientA ?! 9 | *~ >A 'response has timestamp, db metadata'! 10 | 'The metadata object is global and immutable, must match for all replicas.' 11 | 'We use event id for a fresh timestamp which becomes the connection id.' 12 | 'Ref is used for the peer\'s last known event id (0 for no past handshakes).' 13 | *db#test@)2+peer?! 14 | @)1:ClockMode>Logical 15 | :ClockLen=5 16 | 17 | *~ >A 'unknown type'? 18 | 'the general sanity check detects unknown data types' 19 | *weird#type? 20 | *~ >A 'error; no sub'! 21 | 'responds with a closing subscription, error code' 22 | *weird#type@~:WrongType$~~~~~~~~~~! 23 | 24 | *~ >A 'state push'? 25 | 'a client creates an object, pushes the state to the server, subscribes to updates' 26 | *lww#obj@)4+ClientA?! 27 | :one=1 28 | *~ >A 'server acknowledges the write'! 29 | 'the server responds with a query and an ack (the object is readable/writable)' 30 | 'an ack is simply an empty state (header only)' 31 | 'note that the server acknowledges the state it received in the same frame as the query' 32 | *lww#obj?! 33 | @)4+ClientA! 34 | 35 | *~ >B 'new ro handshake, object sub'? 36 | 'this client makes no writes' 37 | *db#test@)5+ClientB? 38 | *lww#obj@)4+ClientA? 39 | *~ >B 'ro response hs, object state'! 40 | 'note the server makes no ? queries' 41 | 'FIXME no ?' 42 | *db#test@)6+peer?! 43 | *lww#obj@)4+ClientA! 44 | 45 | *~ >A 'update op'? 46 | *lww#obj@)5+ClientA:two'2'; 47 | *~ >A 'ack'! 48 | 'strictly speaking, type and object ids are unnecessary here (c>p sync is log based)' 49 | *lww#obj@)5+ClientA! 50 | *~ >B 'relayed op'! 51 | 'there is an acknowledged subscription => the op is relayed' 52 | *lww#obj@)5+ClientA:two'2'; 53 | 54 | *~ >C 'hs, sub'? 55 | *db#test@)A+ClientC?! 56 | *lww#obj?! 57 | *~ >C 'merged state'! 58 | *db#test@)B+peer?! 59 | *lww#obj@)5+ClientA?! @)4:one=1, @)5:two'2' 60 | 61 | *~ >C 'unsub'? 62 | *lww#obj@~? 63 | *~ >C 'unsub OK'! 64 | *lww#obj@~! 65 | 66 | *~ >A 'yet another update op'? 67 | *lww#obj@)6+ClientA:three^3.0; 68 | *~ >A 'ack, time update'! 69 | *lww#obj@)6+ClientA! 70 | *~ >C 'unsubd, no updates' 71 | 72 | . 73 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/001-conn.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@0+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' #object?. 4 | *~ '<' *lww#object@time+author!:key'value'. 5 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/002-hs.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/003-calendar-clock.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+?!. 2 | *~ '<' 3 | *db#test$user@)2+server?! 4 | @)1:ClockMode>Calendar 5 | :ClockLen=5. 6 | 7 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/004-query.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' #object?!. 4 | *~ '<' *lww#object@time+author!:key'value'. 5 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/005-push.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' #object?!. 4 | *~ '<' *lww#object@1ABD+author!:key'value'. 5 | *~ '>' @~?#object,. 6 | *~ '>' *lww#object@1ABD1+user!:bar'biz'. 7 | *~ '>' *lww#object@1ABD2+user!:foo>object. 8 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/006-lwwset.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' #object?!. 4 | *~ '>' *lww#object@1ABC1+user!:username'olebedev'. 5 | *~ '>' *lww#object@1ABC2+user!:email'ole6edev@gmail.com'. 6 | *~ '>' *lww#object@1ABC3+user!:email,. 7 | *~ '>' #1ABC4+user?!. 8 | *~ '>' *lww#object@1ABC5+user!:profile>1ABC4+user. 9 | *~ '<' #1ABC4+user!. 10 | *~ '>' *lww#1ABC4+user@1ABC6+user!:active>true. 11 | *~ '>' *lww#1ABC4+user@1ABC7+user!:active>false. 12 | *~ '<' @1ABC7+user!. 13 | *~ '<' *lww#object@1ABD+olebedev!:profile,. 14 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/007-pending.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' *lww#object@1ABC1+user!:username'olebedev'. 4 | *~ '>' *lww#object@1ABC2+user!:email'ole6edev@gmail.com'. 5 | *~ '>' *lww#object@1ABC3+user!:email,. 6 | *~ '>' *lww#object@1ABC5+user!:profile>1ABC4+user. 7 | *~ '>' *lww#1ABC4+user@1ABC6+user!:active>true. 8 | *~ '>' *lww#1ABC4+user@1ABC7+user!:active>false. 9 | *~ '<' @1ABC6+user!. 10 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/008-setadd.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' #object?!. 4 | *~ '<' #object!. 5 | *~ '>' *set#object@1ABC1+user!=5. 6 | *~ '>' *set#object@1ABC2+user!=5. 7 | *~ '>' *set#object@1ABC3+user!=42. 8 | *~ '<' @1ABC6+user!. 9 | *~ '>' #1ABC8+user?!. 10 | *~ '>' *set#object@1ABC9+user!>1ABC8+user. 11 | *~ '<' #1ABC8+user!. 12 | *~ '>' *set#1ABC8+user@1ABCA+user!=37. 13 | *~ '<' @1ABD+user!. 14 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/009-pending.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' *lww#object@1ABC1+user!:username'olebedev'. 4 | *~ '>' *lww#object@1ABC2+user!:email'ole6edev@gmail.com'. 5 | *~ '>' *lww#object@1ABC3+user!:email,. 6 | *~ '>' *lww#object@1ABC5+user!:profile>1ABC4+user. 7 | *~ '>' *lww#1ABC4+user@1ABC6+user!:active>true. 8 | *~ '>' *lww#1ABC4+user@1ABC7+user!:active>false. 9 | *~ '<' @1ABZ+user!. 10 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/010-setrm.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' #object?!. 4 | *~ '<' #object!. 5 | *~ '>' *set#object@1ABC1+user!=5. 6 | *~ '>' *set#object@1ABC3+user!:1ABC1+user,. 7 | *~ '>' #thisone?!. 8 | *~ '<' *set#thisone@1ABC5+user!=42. 9 | *~ '>' @~?#thisone,. 10 | *~ '>' *set#thisone@1ABC6+user!:1ABC5+user,. 11 | *~ '>' #thisone@1ABC5+user?!. 12 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/011-react.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' #object?!. 4 | *~ '<' *lww#object@1ABC1+user!:test=5. 5 | *~ '>' *lww#object@1ABC2+user!:some'value'. 6 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/012-local-uuids.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' #object?!. 4 | *~ '>' *lww#object@time+author!:key'value'. 5 | *~ '>' *lww#object@time+author!:key'value'. 6 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/013-once.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' #object?!. 4 | *~ '<' *lww#object@time+author!:key'value'. 5 | *~ '>' @~?#object,. 6 | *~ '>' #another?!. 7 | *~ '<' #another!. 8 | *~ '>' @~?#another,. 9 | 10 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/014-gql-subs.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' *lww#1ABC1+user@1ABC3+user!:a=42:b'wat':c^0.1:d>false:e>true:f>1ABC2+user. 4 | *~ '>' *set#1ABC2+user@1ABCE+user!>1ABC4+user. 5 | *~ '>' *lww#1ABC4+user@1ABCF+user!:value=1. 6 | *~ '>' *set#1ABC2+user@1ABCG+user!>1ABC5+user. 7 | *~ '>' *lww#1ABC5+user@1ABCH+user!:value=2. 8 | *~ '>' *set#1ABC2+user@1ABCI+user!>1ABC6+user. 9 | *~ '>' *lww#1ABC6+user@1ABCJ+user!:value=3. 10 | *~ '>' *set#1ABC2+user@1ABCK+user!>1ABC7+user. 11 | *~ '>' *lww#1ABC7+user@1ABCL+user!:value=4. 12 | *~ '>' *set#1ABC2+user@1ABCM+user!>1ABC8+user. 13 | *~ '>' *lww#1ABC8+user@1ABCN+user!:value=5. 14 | *~ '>' *set#1ABC2+user@1ABCO+user!>1ABC9+user. 15 | *~ '>' *lww#1ABC9+user@1ABCP+user!:value=6. 16 | *~ '>' *set#1ABC2+user@1ABCS+user!>1ABCB+user. 17 | *~ '>' *lww#1ABCB+user@1ABCT+user!:value=8. 18 | *~ '>' *set#1ABC2+user@1ABCU+user!>1ABCC+user. 19 | *~ '>' *lww#1ABCC+user@1ABCV+user!:value=9. 20 | *~ '>' *set#1ABC2+user@1ABCW+user!>1ABCD+user. 21 | *~ '>' *lww#1ABCD+user@1ABCX+user!:value=10. 22 | *~ '>' #1ABC1+user?!. 23 | *~ '>' #1ABC2+user?!. 24 | *~ '>' #nope?!. 25 | *~ '>' #1ABCB+user?!#(9+?!#(8+?!#(7+?!#(6+?!. 26 | *~ '>' *set#1ABC2+user@1ABCZ+user!>1ABCY+user. 27 | *~ '>' *lww#1ABCY+user@1ABC_+user!:value=11. 28 | *~ '>' @~?#1ABC6+user,. 29 | *~ '>' #1ABCC+user?!. 30 | *~ '>' *lww#nope@1ABCa+user!:test=1. 31 | *~ '>' @~?#1ABC1+user,#(2+,#nope,#1ABCB+user,#(9+,#(8+,#(7+,#(C+,. 32 | *~ '>' *lww#nope@1ABCb+user!:test=2. 33 | *~ '<' @1ABCX+user!. 34 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/015-gql-mutation.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' *lww#1ABC1+user@1ABC2+user!:test=1. 4 | *~ '>' *lww#1ABC1+user@1ABC3+user!:hello'world'. 5 | *~ '<' @1ABC3+user!. 6 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/016-gql-query.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' *lww#1ABC1+user@1ABC3+user!:a=42:b'wat':c^0.1:d>false:e>true:f>1ABC2+user. 4 | *~ '>' *set#1ABC2+user@1ABCE+user!>1ABC4+user. 5 | *~ '>' *lww#1ABC4+user@1ABCF+user!:value=1. 6 | *~ '>' *set#1ABC2+user@1ABCG+user!>1ABC5+user. 7 | *~ '>' *lww#1ABC5+user@1ABCH+user!:value=2. 8 | *~ '>' *set#1ABC2+user@1ABCI+user!>1ABC6+user. 9 | *~ '>' *lww#1ABC6+user@1ABCJ+user!:value=3. 10 | *~ '>' *set#1ABC2+user@1ABCK+user!>1ABC7+user. 11 | *~ '>' *lww#1ABC7+user@1ABCL+user!:value=4. 12 | *~ '>' *set#1ABC2+user@1ABCM+user!>1ABC8+user. 13 | *~ '>' *lww#1ABC8+user@1ABCN+user!:value=5. 14 | *~ '>' *set#1ABC2+user@1ABCO+user!>1ABC9+user. 15 | *~ '>' *lww#1ABC9+user@1ABCP+user!:value=6. 16 | *~ '>' *set#1ABC2+user@1ABCS+user!>1ABCB+user. 17 | *~ '>' *lww#1ABCB+user@1ABCT+user!:value=8. 18 | *~ '>' *set#1ABC2+user@1ABCU+user!>1ABCC+user. 19 | *~ '>' *lww#1ABCC+user@1ABCV+user!:value=9. 20 | *~ '>' *set#1ABC2+user@1ABCW+user!>1ABCD+user. 21 | *~ '>' *lww#1ABCD+user@1ABCX+user!:value=10. 22 | *~ '>' #1ABC1+user@1ABC3+user?!. 23 | *~ '>' #1ABC2+user@1ABCW+user?!#nope@0?!. 24 | *~ '>' #1ABCB+user@1ABCT+user?!#(9+@(P+?!#(8+@(N+?!#(7+@(L+?!#(6+@(J+?!. 25 | *~ '>' *set#1ABC2+user@1ABCZ+user!>1ABCY+user. 26 | *~ '>' @~?#1ABC6+user,. 27 | *~ '>' *lww#1ABCY+user@1ABC_+user!:value=11. 28 | *~ '>' #1ABCC+user@1ABCV+user?!. 29 | *~ '>' *lww#nope@1ABCa+user!:test=1. 30 | *~ '>' @~?#1ABC1+user,#(2+,#(C+,#(B+,#(9+,#(8+,#(7+,#nope,. 31 | *~ '>' *lww#nope@1ABCb+user!:test=2. 32 | *~ '<' @1ABCX+user!. 33 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/017-ensure-directive.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' *lww#1ABC1+user@1ABC3+user!:collection>1ABC2+user. 4 | *~ '>' *set#1ABC2+user@1ABC5+user!>1ABC4+user. 5 | *~ '>' *lww#1ABC4+user@1ABC6+user!:value=1. 6 | *~ '>' #1ABC1+user@1ABC3+user?!. 7 | *~ '>' #1ABC2+user@1ABC5+user?!#nope@0?!. 8 | *~ '<' #nope!. 9 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/018-react-graphql.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+user!. 3 | *~ '>' #object?!. 4 | *~ '<' *lww#object@1ABC1+user!:test=5. 5 | *~ '>' *lww#object@1ABC2+user!:some'value'. 6 | *~ '>' *lww#object@1ABC3+user!:test=6. 7 | *~ '>' *lww#object@1ABC4+user!:additional>true. 8 | -------------------------------------------------------------------------------- /packages/__tests__/fixtures/019-pending.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+?!'JwT.t0k.en'. -------------------------------------------------------------------------------- /packages/__tests__/fixtures/020-empty-ack.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' #ack?!. 4 | *~ '<' #ack! -------------------------------------------------------------------------------- /packages/__tests__/fixtures/021-gql-empty-ack.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '>' #ack?!. 4 | *~ '<' #ack!. -------------------------------------------------------------------------------- /packages/__tests__/fixtures/022-client-seen.ron: -------------------------------------------------------------------------------- 1 | *~ '>' *db#test@+user?!'JwT.t0k.en'. 2 | *~ '<' *db#test$user@1ABC+server!. 3 | *~ '<' *lww#1ABC1+user@1ABC_+user!:a=42:b'wat':c^0.1:d>false:e>true:f>1ABC2+user. -------------------------------------------------------------------------------- /packages/__tests__/fixtures/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { readFileSync } from 'fs'; 4 | import { join } from 'path'; 5 | 6 | import Op, { Frame } from '@swarm/ron'; 7 | import UUID, { ZERO } from '@swarm/ron-uuid'; 8 | import type { Connection as IConn } from '../../client/src'; 9 | 10 | let id = 1; 11 | 12 | export class Connection implements IConn { 13 | id: string; 14 | fixtures: Array; 15 | session: Array; 16 | onmessage: (ev: MessageEvent) => any; 17 | onopen: (ev: Event) => any; 18 | readyState: number; 19 | 20 | open(): void {} 21 | constructor(fixtures: ?string) { 22 | this.id = `#${id++} ${fixtures || ''}`; 23 | this.fixtures = []; 24 | this.session = []; 25 | if (fixtures) { 26 | const content = readFileSync(join(__dirname, fixtures), 'utf8'); 27 | for (const chunk of content.split('.\n')) { 28 | if (!chunk.trim()) continue; 29 | const frame = new Frame(chunk); 30 | for (const op of frame) { 31 | if (op.isComment() && op.source) { 32 | this.fixtures.push( 33 | // $FlowFixMe 34 | new RawFrame(frame.body.slice(op.source.length), op.value(0)), 35 | ); 36 | } else { 37 | throw new Error('unexpected op'); 38 | } 39 | break; 40 | } 41 | } 42 | } 43 | setTimeout(() => { 44 | if (this.onopen) this.onopen(new Event('')); 45 | this.pushPending(); 46 | }, 0); 47 | } 48 | 49 | dump(): { fixtures: Array, session: Array } { 50 | return { 51 | fixtures: this.fixtures, 52 | session: this.session, 53 | }; 54 | } 55 | 56 | send(payload: string): void { 57 | this.session.push(new RawFrame(payload, '>')); 58 | // console.log(`[${this.id}] connection.send('${payload}')`); 59 | this.pushPending(); 60 | } 61 | 62 | pushPending(): void { 63 | let i = 0; 64 | for (const raw of this.fixtures.slice(this.session.length)) { 65 | i++; 66 | if (raw.direction === '<') { 67 | (raw => { 68 | this.session.push(raw); 69 | setTimeout(() => { 70 | // console.log(`[${this.id}] #${i} session.push('${raw.toString()}')`); 71 | this.onmessage((({ data: raw.body }: any): MessageEvent)); 72 | // console.log('message was sent'); 73 | }, 100 << i); 74 | })(raw); 75 | } else break; 76 | } 77 | } 78 | 79 | close() {} 80 | } 81 | 82 | test('connection', () => { 83 | const conn = new Connection('001-conn.ron'); 84 | const dump = conn.dump(); 85 | expect(JSON.stringify(dump.fixtures)).toBe( 86 | "[\"*~ '>' *db#test@0+user?!'JwT.t0k.en'.\"," + 87 | '"*~ \'<\' *db#test$user@1ABC+user!.",' + 88 | '"*~ \'>\' #object?.",' + 89 | "\"*~ '<' *lww#object@time+author!:key'value'.\"]", 90 | ); 91 | expect(dump.fixtures[0].direction).toBe('>'); 92 | }); 93 | 94 | class RawFrame { 95 | direction: string; 96 | body: string; 97 | 98 | constructor(body: string, direction: string) { 99 | this.body = body; 100 | this.direction = direction; 101 | } 102 | 103 | toString(): string { 104 | return `*~ '${this.direction}' ${this.body}.`; 105 | } 106 | 107 | toJSON(): string { 108 | return this.toString(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/api/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/api/__tests__/lww.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Frame, UUID } from '../../ron/src'; 4 | import { Connection } from '../../__tests__/fixtures'; 5 | import API from '../src'; 6 | import { InMemory } from '../../client/src/storage'; 7 | 8 | test('api.set(...)', async () => { 9 | const storage = new InMemory(); 10 | const api = new API({ 11 | storage, 12 | upstream: new Connection('006-lwwset.ron'), 13 | db: { 14 | id: 'user', 15 | name: 'test', 16 | auth: 'JwT.t0k.en', 17 | clockMode: 'Logical', 18 | }, 19 | }); 20 | 21 | await api.ensure(); 22 | let obj = []; 23 | function cbk(id, state) { 24 | obj.push({ id, state }); 25 | } 26 | await api.client.on('#object', cbk); 27 | 28 | let set = await api.set('object', { username: 'olebedev' }); 29 | expect(storage.storage['object']).toBe( 30 | "*lww#object@1ABC1+user!:username'olebedev'", 31 | ); 32 | set = await api.set('object', { email: 'ole6edev@gmail.com' }); 33 | expect(storage.storage['object']).toBe( 34 | "*lww#object@1ABC2+user!:email'ole6edev@gmail.com'@(1+:username'olebedev'", 35 | ); 36 | set = await api.set('object', { email: undefined }); 37 | expect(storage.storage['object']).toBe( 38 | "*lww#object@1ABC3+user!:email,@(1+:username'olebedev'", 39 | ); 40 | 41 | expect(Object.keys(api.client.lstn)).toEqual(['object']); 42 | expect(api.client.lstn['object']).toHaveLength(1); 43 | 44 | const profileUUID = api.uuid(); 45 | await api.client.on('#' + profileUUID.toString(), cbk); 46 | set = await api.set('object', { profile: profileUUID }); 47 | 48 | expect(storage.storage['object']).toBe( 49 | "*lww#object@1ABC5+user!@(3+:email,@(5+:profile>1ABC4+user@(1+:username'olebedev'", 50 | ); 51 | 52 | expect(obj).toEqual([ 53 | { id: '#object', state: null }, 54 | { id: '#object', state: "*lww#object@1ABC1+user!:username'olebedev'" }, 55 | { 56 | id: '#object', 57 | state: 58 | "*lww#object@1ABC2+user!:email'ole6edev@gmail.com'@(1+:username'olebedev'", 59 | }, 60 | { 61 | id: '#object', 62 | state: "*lww#object@1ABC3+user!:email,@(1+:username'olebedev'", 63 | }, 64 | { id: '#1ABC4+user', state: null }, 65 | { 66 | id: '#object', 67 | state: 68 | "*lww#object@1ABC5+user!@(3+:email,@(5+:profile>1ABC4+user@(1+:username'olebedev'", 69 | }, 70 | ]); 71 | 72 | set = await api.set(profileUUID.toString(), { active: true }); 73 | expect(storage.storage[profileUUID.toString()]).toBe( 74 | '*lww#1ABC4+user@1ABC6+user!:active>true', 75 | ); 76 | 77 | expect(obj).toEqual([ 78 | { id: '#object', state: null }, 79 | { id: '#object', state: "*lww#object@1ABC1+user!:username'olebedev'" }, 80 | { 81 | id: '#object', 82 | state: 83 | "*lww#object@1ABC2+user!:email'ole6edev@gmail.com'@(1+:username'olebedev'", 84 | }, 85 | { 86 | id: '#object', 87 | state: "*lww#object@1ABC3+user!:email,@(1+:username'olebedev'", 88 | }, 89 | { id: '#1ABC4+user', state: null }, 90 | { 91 | id: '#object', 92 | state: 93 | "*lww#object@1ABC5+user!@(3+:email,@(5+:profile>1ABC4+user@(1+:username'olebedev'", 94 | }, 95 | { id: '#1ABC4+user', state: '*lww#1ABC4+user@1ABC6+user!:active>true' }, 96 | ]); 97 | 98 | expect(api.client.lstn['object']).toEqual(api.client.lstn['1ABC4+user']); 99 | 100 | set = await api.set(profileUUID.toString(), { active: false }); 101 | expect(storage.storage[profileUUID.toString()]).toBe( 102 | '*lww#1ABC4+user@1ABC7+user!:active>false', 103 | ); 104 | 105 | expect(obj).toEqual([ 106 | { id: '#object', state: null }, 107 | { id: '#object', state: "*lww#object@1ABC1+user!:username'olebedev'" }, 108 | { 109 | id: '#object', 110 | state: 111 | "*lww#object@1ABC2+user!:email'ole6edev@gmail.com'@(1+:username'olebedev'", 112 | }, 113 | { 114 | id: '#object', 115 | state: "*lww#object@1ABC3+user!:email,@(1+:username'olebedev'", 116 | }, 117 | { id: '#1ABC4+user', state: null }, 118 | { 119 | id: '#object', 120 | state: 121 | "*lww#object@1ABC5+user!@(3+:email,@(5+:profile>1ABC4+user@(1+:username'olebedev'", 122 | }, 123 | { id: '#1ABC4+user', state: '*lww#1ABC4+user@1ABC6+user!:active>true' }, 124 | { id: '#1ABC4+user', state: '*lww#1ABC4+user@1ABC7+user!:active>false' }, 125 | ]); 126 | 127 | // due to async nature of connection mock 128 | await new Promise(r => setTimeout(r, 500)); 129 | 130 | // $FlowFixMe 131 | const dump = api.client.upstream.dump(); 132 | expect(dump.session).toEqual(dump.fixtures); 133 | // $FlowFixMe 134 | expect(api.client.storage.storage['1ABC4+user']).toBe( 135 | '*lww#1ABC4+user@1ABC7+user!:active>false', 136 | ); 137 | // $FlowFixMe 138 | expect(JSON.parse(api.client.storage.storage.__meta__)).toEqual({ 139 | name: 'test', 140 | clockLen: 5, 141 | forkMode: '// FIXME', 142 | peerIdBits: 30, 143 | horizont: 604800, 144 | auth: 'JwT.t0k.en', 145 | clockMode: 'Logical', 146 | id: 'user', 147 | offset: 0, 148 | }); 149 | // $FlowFixMe 150 | expect(JSON.parse(api.client.storage.storage.__pending__)).toEqual([]); 151 | expect(storage.storage.object).toBe( 152 | "*lww#object@1ABD+olebedev!@1ABC3+user:email,@1ABD+olebedev:profile,@1ABC1+user:username'olebedev'", 153 | ); 154 | expect(api.uuid().toString()).toBe('1ABD1+user'); 155 | 156 | expect(obj).toEqual([ 157 | { id: '#object', state: null }, 158 | { id: '#object', state: "*lww#object@1ABC1+user!:username'olebedev'" }, 159 | { 160 | id: '#object', 161 | state: 162 | "*lww#object@1ABC2+user!:email'ole6edev@gmail.com'@(1+:username'olebedev'", 163 | }, 164 | { 165 | id: '#object', 166 | state: "*lww#object@1ABC3+user!:email,@(1+:username'olebedev'", 167 | }, 168 | { id: '#1ABC4+user', state: null }, 169 | { 170 | id: '#object', 171 | state: 172 | "*lww#object@1ABC5+user!@(3+:email,@(5+:profile>1ABC4+user@(1+:username'olebedev'", 173 | }, 174 | { id: '#1ABC4+user', state: '*lww#1ABC4+user@1ABC6+user!:active>true' }, 175 | { id: '#1ABC4+user', state: '*lww#1ABC4+user@1ABC7+user!:active>false' }, 176 | { 177 | id: '#object', 178 | state: 179 | "*lww#object@1ABD+olebedev!@1ABC3+user:email,@1ABD+olebedev:profile,@1ABC1+user:username'olebedev'", 180 | }, 181 | ]); 182 | 183 | set = await api.set('object', { local: UUID.fromString('test').local() }); 184 | expect(storage.storage.object).toBe( 185 | "*lww#object@1ABD+olebedev!@1ABC3+user:email,@1ABD+olebedev:profile,@1ABC1+user:username'olebedev'", 186 | ); 187 | expect(set).toBeFalsy(); 188 | }); 189 | -------------------------------------------------------------------------------- /packages/api/__tests__/new.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {Frame, UUID} from '../../ron/src'; 4 | import {Connection} from '../../__tests__/fixtures'; 5 | import API from '../src'; 6 | import {InMemory} from '../../client/src/storage'; 7 | 8 | test('new API(...)', async () => { 9 | const api = new API({ 10 | storage: new InMemory(), 11 | upstream: new Connection('002-hs.ron'), 12 | db: { 13 | id: 'user', 14 | name: 'test', 15 | auth: 'JwT.t0k.en', 16 | clockMode: 'Logical', 17 | }, 18 | }); 19 | 20 | await api.ensure(); 21 | // $FlowFixMe 22 | let dump = api.client.upstream.dump(); 23 | expect(dump.session).toEqual(dump.fixtures); 24 | // $FlowFixMe 25 | expect(JSON.parse(api.client.storage.storage.__meta__)).toEqual({ 26 | name: 'test', 27 | clockLen: 5, 28 | forkMode: '// FIXME', 29 | peerIdBits: 30, 30 | horizont: 604800, 31 | auth: 'JwT.t0k.en', 32 | clockMode: 'Logical', 33 | id: 'user', 34 | offset: 0, 35 | }); 36 | expect(api.uuid().toString()).toBe('1ABC1+user'); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/api/__tests__/set.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Frame, UUID } from '../../ron/src'; 4 | import { Connection } from '../../__tests__/fixtures'; 5 | import API from '../src'; 6 | import { InMemory } from '../../client/src/storage'; 7 | 8 | test('set.add(...)', async () => { 9 | const storage = new InMemory(); 10 | const api = new API({ 11 | storage, 12 | upstream: new Connection('008-setadd.ron'), 13 | db: { 14 | id: 'user', 15 | name: 'test', 16 | auth: 'JwT.t0k.en', 17 | clockMode: 'Logical', 18 | }, 19 | }); 20 | 21 | await api.ensure(); 22 | 23 | let obj = []; 24 | function cbk(id: string, state: string | null) { 25 | obj.push({ id, state }); 26 | } 27 | 28 | await api.client.on('#object', cbk); 29 | await new Promise(r => setTimeout(r, 300)); 30 | let ok = await api.add('object', 5); 31 | expect(ok).toBeTruthy(); 32 | 33 | expect(obj).toEqual([ 34 | { id: '#object', state: null }, 35 | { id: '#object', state: '' }, 36 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 37 | ]); 38 | 39 | await api.add('object', 5); 40 | expect(obj).toEqual([ 41 | { id: '#object', state: null }, 42 | { id: '#object', state: '' }, 43 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 44 | { id: '#object', state: '*set#object@1ABC2+user!=5@(1+=5' }, 45 | ]); 46 | 47 | ok = await api.add('object', 42); 48 | expect(ok).toBeTruthy(); 49 | expect(obj).toEqual([ 50 | { id: '#object', state: null }, 51 | { id: '#object', state: '' }, 52 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 53 | { id: '#object', state: '*set#object@1ABC2+user!=5@(1+=5' }, 54 | { id: '#object', state: '*set#object@1ABC3+user!=42@(2+=5@(1+=5' }, 55 | ]); 56 | 57 | await new Promise(r => setTimeout(r, 500)); 58 | expect(storage.storage.__pending__).toBe('[]'); 59 | expect(api.uuid().toString()).toBe('1ABC7+user'); 60 | 61 | const sub = api.uuid(); 62 | await api.client.on('#' + sub.toString(), cbk); 63 | await api.add('object', sub); 64 | expect(obj).toEqual([ 65 | { id: '#object', state: null }, 66 | { id: '#object', state: '' }, 67 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 68 | { id: '#object', state: '*set#object@1ABC2+user!=5@(1+=5' }, 69 | { id: '#object', state: '*set#object@1ABC3+user!=42@(2+=5@(1+=5' }, 70 | { id: '#1ABC8+user', state: null }, 71 | { 72 | id: '#object', 73 | state: '*set#object@1ABC9+user!>1ABC8+user@(3+=42@(2+=5@(1+=5', 74 | }, 75 | ]); 76 | 77 | await new Promise(r => setTimeout(r, 300)); 78 | 79 | await api.add(sub, 37); 80 | expect(obj).toEqual([ 81 | { id: '#object', state: null }, 82 | { id: '#object', state: '' }, 83 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 84 | { id: '#object', state: '*set#object@1ABC2+user!=5@(1+=5' }, 85 | { id: '#object', state: '*set#object@1ABC3+user!=42@(2+=5@(1+=5' }, 86 | { id: '#1ABC8+user', state: null }, 87 | { 88 | id: '#object', 89 | state: '*set#object@1ABC9+user!>1ABC8+user@(3+=42@(2+=5@(1+=5', 90 | }, 91 | { id: '#1ABC8+user', state: '' }, 92 | { id: '#1ABC8+user', state: '*set#1ABC8+user@1ABCA+user!=37' }, 93 | ]); 94 | 95 | await new Promise(r => setTimeout(r, 300)); 96 | 97 | // $FlowFixMe 98 | const dump = api.client.upstream.dump(); 99 | expect(dump.session).toEqual(dump.fixtures); 100 | expect(storage.storage.object).toBe( 101 | '*set#object@1ABC9+user!>1ABC8+user@(3+=42@(2+=5@(1+=5', 102 | ); 103 | 104 | const add = await api.add('object', UUID.fromString('test').local()); 105 | expect(storage.storage.object).toBe( 106 | '*set#object@1ABC9+user!>1ABC8+user@(3+=42@(2+=5@(1+=5', 107 | ); 108 | expect(add).toBeFalsy(); 109 | }); 110 | 111 | test('set.remove(...)', async () => { 112 | const storage = new InMemory(); 113 | const api = new API({ 114 | storage, 115 | upstream: new Connection('010-setrm.ron'), 116 | db: { 117 | id: 'user', 118 | name: 'test', 119 | auth: 'JwT.t0k.en', 120 | clockMode: 'Logical', 121 | }, 122 | }); 123 | 124 | await api.ensure(); 125 | 126 | let obj = []; 127 | function cbk(id: string, state: string | null) { 128 | obj.push({ id, state }); 129 | } 130 | await api.client.on('#object', cbk); 131 | 132 | await new Promise(r => setTimeout(r, 500)); 133 | 134 | await api.add('object', 5); 135 | expect(obj).toEqual([ 136 | { id: '#object', state: null }, 137 | { id: '#object', state: '' }, 138 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 139 | ]); 140 | 141 | let rm = await api.remove('object', 4); 142 | expect(rm).toBeFalsy(); 143 | expect(obj).toEqual([ 144 | { id: '#object', state: null }, 145 | { id: '#object', state: '' }, 146 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 147 | ]); 148 | 149 | expect(storage.storage.object).toBe('*set#object@1ABC1+user!=5'); 150 | 151 | rm = await api.remove('object', 5); 152 | expect(rm).toBeTruthy(); 153 | expect(obj).toEqual([ 154 | { id: '#object', state: null }, 155 | { id: '#object', state: '' }, 156 | { id: '#object', state: '*set#object@1ABC1+user!=5' }, 157 | { id: '#object', state: '*set#object@1ABC3+user!:1ABC1+user,' }, 158 | ]); 159 | 160 | // $FlowFixMe 161 | expect(api.client.lstn['thisone']).toBeUndefined(); 162 | await new Promise(resolve => { 163 | api.client.on('#thisone', resolve, { ensure: true, once: true }); 164 | }); 165 | 166 | rm = await api.remove('thisone', 42); 167 | expect(rm).toBeTruthy(); 168 | 169 | const thisone = await new Promise(r => { 170 | api.client.on('#thisone', (id: string, state: string | null) => { 171 | r({ id, state }); 172 | }); 173 | }); 174 | expect(thisone).toEqual({ 175 | id: '#thisone', 176 | state: '*set#thisone@1ABC6+user!:1ABC5+user,', 177 | }); 178 | 179 | await new Promise(r => setTimeout(r, 300)); 180 | // $FlowFixMe 181 | const dump = api.client.upstream.dump(); 182 | expect(dump.session).toEqual(dump.fixtures); 183 | expect(storage.storage.object).toBe('*set#object@1ABC3+user!:1ABC1+user,'); 184 | }); 185 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/api", 3 | "version": "0.1.1", 4 | "description": "Swarm API", 5 | "author": "Oleg Lebedev (https://github.com/olebedev)", 6 | "main": "lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "" 10 | }, 11 | "dependencies": { 12 | "@swarm/client": "^0.1.1", 13 | "@swarm/rdt": "^0.1.1", 14 | "@swarm/ron": "^0.1.1", 15 | "@swarm/ron-uuid": "^0.1.1", 16 | "object-hash": "^1.2.0", 17 | "regenerator-runtime": "^0.11.1" 18 | }, 19 | "files": [ 20 | "lib/*.js", 21 | "lib/*.js.flow" 22 | ], 23 | "scripts": { 24 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 25 | "build:clean": "../../node_modules/.bin/rimraf lib", 26 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 27 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 28 | }, 29 | "keywords": [ 30 | "swarm", 31 | "replicated", 32 | "RON", 33 | "CRDT" 34 | ], 35 | "publishConfig": { 36 | "access": "public" 37 | }, 38 | "license": "MIT" 39 | } 40 | -------------------------------------------------------------------------------- /packages/api/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import regeneratorRuntime from 'regenerator-runtime'; 4 | import hash from 'object-hash'; 5 | 6 | import Client from '@swarm/client'; 7 | import Op, { Frame, ZERO, UUID, FRAME_SEP, js2ron } from '@swarm/ron'; 8 | import { ZERO as ZERO_UUID } from '@swarm/ron-uuid'; 9 | import { lww, set, ron2js } from '@swarm/rdt'; 10 | import type { Atom } from '@swarm/ron'; 11 | import type { Options as ClntOpts } from '@swarm/client'; 12 | 13 | export type Options = ClntOpts & { 14 | gcPeriod?: number, 15 | strictMode?: boolean, 16 | }; 17 | 18 | export type Value = { [string]: Atom | Value } | Value[] | null; 19 | 20 | interface Subscription { 21 | off(): boolean; 22 | is(hash: string): boolean; 23 | } 24 | 25 | export default class API { 26 | client: Client; 27 | options: Options; 28 | subs: Array; 29 | cache: { [string]: { [string]: Atom } }; 30 | gcInterval: IntervalID; 31 | 32 | constructor(options: Options): API { 33 | this.client = new Client(options); 34 | this.options = options; 35 | this.subs = []; 36 | this.cache = {}; 37 | if (options.gcPeriod) { 38 | this.gcInterval = setInterval(this.gc.bind(this), options.gcPeriod); 39 | } 40 | // $FlowFixMe 41 | this.uuid = this.uuid.bind(this); 42 | return this; 43 | } 44 | 45 | ensure(): Promise { 46 | return this.client.ensure(); 47 | } 48 | 49 | uuid(): UUID { 50 | if (!this.client.clock) 51 | throw new Error( 52 | 'have no clock yet, invoke `await .ensure()` first', 53 | ); 54 | return this.client.clock.time(); 55 | } 56 | 57 | // garbage collection for unused cached data 58 | gc(): {| deleted: number, existing: number |} { 59 | const ret = { deleted: 0, existing: 0 }; 60 | const lstnrs = {}; 61 | for (const id of Object.keys(this.client.lstn)) lstnrs[id] = true; 62 | 63 | for (const id of Object.keys(this.cache)) { 64 | if (!lstnrs[id]) { 65 | delete this.cache[id]; 66 | ret.deleted++; 67 | } else { 68 | ret.existing++; 69 | } 70 | } 71 | 72 | return ret; 73 | } 74 | 75 | async set( 76 | id: string | UUID, 77 | value: { [string]: Atom | void }, 78 | ): Promise { 79 | if (!id) return false; 80 | const uuid = id instanceof UUID ? id : UUID.fromString(id); 81 | await this.client.ensure(); 82 | 83 | if (this.options.strictMode) { 84 | const type = await this.typeOf(id); 85 | if (type && type !== lww.type.toString()) return false; 86 | } 87 | 88 | const frame = new Frame(); 89 | 90 | frame.push( 91 | new Op(lww.type, uuid, this.uuid(), ZERO_UUID, undefined, FRAME_SEP), 92 | ); 93 | 94 | for (const k of Object.keys(value).sort()) { 95 | const op = new Op( 96 | lww.type, 97 | uuid, 98 | frame.last.uuid(2), 99 | UUID.fromString(k), 100 | undefined, 101 | ',', 102 | ); 103 | 104 | if (value[k] !== undefined) { 105 | op.values = js2ron([value[k]]); 106 | if (!uuid.isLocal() && value[k] instanceof UUID && value[k].isLocal()) { 107 | return false; 108 | } 109 | } 110 | 111 | frame.push(op); 112 | } 113 | 114 | if (frame.isPayload()) { 115 | await this.client.push(frame.toString()); 116 | return true; 117 | } 118 | return false; 119 | } 120 | 121 | async add(id: string | UUID, value: Atom): Promise { 122 | if (!id) return false; 123 | const uuid = id instanceof UUID ? id : UUID.fromString(id); 124 | await this.client.ensure(); 125 | 126 | if (this.options.strictMode) { 127 | const type = await this.typeOf(id); 128 | if (type && type !== set.type.toString()) return false; 129 | } 130 | 131 | const frame = new Frame(); 132 | const time = this.uuid(); 133 | let op = new Op(set.type, uuid, time, ZERO_UUID, undefined, FRAME_SEP); 134 | frame.push(op); 135 | op = op.clone(); 136 | 137 | if (!uuid.isLocal() && value instanceof UUID && value.isLocal()) { 138 | return false; 139 | } 140 | 141 | op.values = js2ron([value]); 142 | frame.pushWithTerm(op, ','); 143 | 144 | await this.client.push(frame.toString()); 145 | return true; 146 | } 147 | 148 | async remove(id: string | UUID, value: Atom): Promise { 149 | if (!id) return false; 150 | const uuid = id instanceof UUID ? id : UUID.fromString(id); 151 | id = uuid.toString(); 152 | await this.client.ensure(); 153 | 154 | if (this.options.strictMode) { 155 | const type = await this.typeOf(id); 156 | if (type !== set.type.toString()) return false; 157 | } 158 | 159 | const frame = new Frame(); 160 | const ts = this.uuid(); 161 | let deleted = false; 162 | let op = new Op(set.type, uuid, ts, ZERO_UUID, undefined, FRAME_SEP); 163 | frame.push(op); 164 | 165 | let state = await this.client.storage.get(id); 166 | if (!state) return false; 167 | 168 | const str = js2ron([value]); 169 | for (const v of new Frame(state)) { 170 | if (!v.isRegular()) continue; 171 | if (v.values === str) { 172 | deleted = true; 173 | op = op.clone(); 174 | op.location = v.event; 175 | op.values = ''; 176 | frame.pushWithTerm(op, ','); 177 | } 178 | } 179 | 180 | if (deleted) { 181 | await this.client.push(frame.toString()); 182 | } 183 | return deleted; 184 | } 185 | 186 | close(): Promise { 187 | return this.client.close(); 188 | } 189 | 190 | open(): void { 191 | return this.client.open(); 192 | } 193 | 194 | async typeOf(id: string | UUID): Promise { 195 | const obj = this.cache[id.toString()]; 196 | if (obj !== undefined) { 197 | // found in cache 198 | return obj && obj.type && typeof obj.type === 'string' ? obj.type : ''; 199 | } 200 | 201 | const state = await this.client.storage.get(id.toString()); 202 | if (state) { 203 | const op = Op.fromString(state); 204 | if (op) return op.uuid(0).toString(); 205 | } 206 | // type is not defined 207 | return null; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/client/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # Baseline Swarm Client 2 | 3 | keeps data in memory 4 | -------------------------------------------------------------------------------- /packages/client/__tests__/init.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Frame, UUID } from '../../ron/src'; 4 | import { Connection } from '../../__tests__/fixtures'; 5 | import Client from '../src'; 6 | import { InMemory } from '../src/storage'; 7 | 8 | test('Client: new', async () => { 9 | const client = new Client({ 10 | storage: new InMemory(), 11 | upstream: new Connection('002-hs.ron'), 12 | db: { 13 | id: 'user', 14 | name: 'test', 15 | auth: 'JwT.t0k.en', 16 | clockMode: 'Logical', 17 | }, 18 | }); 19 | 20 | await client.ensure(); 21 | // $FlowFixMe 22 | let dump = client.upstream.dump(); 23 | expect(dump.session).toEqual(dump.fixtures); 24 | // $FlowFixMe 25 | expect(JSON.parse(client.storage.storage.__meta__)).toEqual({ 26 | name: 'test', 27 | clockLen: 5, 28 | forkMode: '// FIXME', 29 | peerIdBits: 30, 30 | horizont: 604800, 31 | auth: 'JwT.t0k.en', 32 | clockMode: 'Logical', 33 | id: 'user', 34 | offset: 0, 35 | }); 36 | // $FlowFixMe 37 | expect(client.clock.last().toString()).toBe('1ABC+server'); 38 | expect(client.clock && client.clock.time().toString()).toBe('1ABC1+user'); 39 | }); 40 | 41 | test('Client: reconnect - init before connnection', async () => { 42 | const storage = new InMemory(); 43 | const meta = 44 | '{"name":"test","clockLen":5,"forkMode":"// FIXME","peerIdBits":30,"horizont":604800,' + 45 | '"credentials":{"password":"12345"},"clockMode":"Logical"}'; 46 | await storage.set('__meta__', meta); 47 | 48 | const client = new Client({ 49 | id: 'user', 50 | storage, 51 | upstream: new Connection('002-hs.ron'), 52 | }); 53 | 54 | await client.ensure(); 55 | }); 56 | 57 | test('Client: w/o clock/url/connection', async () => { 58 | const client = new Client({ id: 'user', storage: new InMemory() }); 59 | try { 60 | await client.ensure(); 61 | } catch (e) { 62 | expect(e).toEqual( 63 | new Error('neither connection options nor clock options found'), 64 | ); 65 | } 66 | }); 67 | 68 | test('Client: not supported clock', async () => { 69 | const client = new Client({ 70 | storage: new InMemory(), 71 | db: { 72 | id: 'user', 73 | name: 'test', 74 | // $FlowFixMe 75 | clockMode: 'Epoch', 76 | }, 77 | }); 78 | 79 | try { 80 | await client.ensure(); 81 | expect('~').toBe("this section mustn't be executed"); 82 | } catch (e) { 83 | expect(e).toEqual( 84 | new Error("TODO: Clock mode 'Epoch' is not supported yet"), 85 | ); 86 | } 87 | }); 88 | 89 | test('Client: assigned id', async () => { 90 | const conn = new Connection('003-calendar-clock.ron'); 91 | const client = new Client({ 92 | storage: new InMemory(), 93 | upstream: conn, 94 | db: { 95 | name: 'test', 96 | }, 97 | }); 98 | 99 | await client.ensure(); 100 | // $FlowFixMe 101 | expect(client.clock.origin()).toBe('user'); 102 | }); 103 | -------------------------------------------------------------------------------- /packages/client/__tests__/misc.js: -------------------------------------------------------------------------------- 1 | import Client, { InMemory } from '../src'; 2 | import UUID from '@swarm/ron-uuid'; 3 | import { Connection } from '../../__tests__/fixtures'; 4 | 5 | // @flow 6 | 7 | describe('Client', () => { 8 | test('last seen', async () => { 9 | const client = new Client({ 10 | storage: new InMemory(), 11 | upstream: new Connection('022-client-seen.ron'), 12 | db: { 13 | id: 'user', 14 | name: 'test', 15 | auth: 'JwT.t0k.en', 16 | clockMode: 'Logical', 17 | }, 18 | }); 19 | 20 | await client.ensure(); 21 | await new Promise(r => setTimeout(r, 200)); 22 | expect(client.seen).toEqual(UUID.fromString('1ABC_+user')); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/client/__tests__/mutex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Mutex from '../src/mutex'; 4 | 5 | describe('mutex.lock(...)', () => { 6 | const mutex = new Mutex(); 7 | 8 | test('returns a fulfilled promise if the given function succeeds', async () => { 9 | const res = await mutex.lock(undefined, () => 'abc'); 10 | expect(res).toBe('abc'); 11 | }); 12 | 13 | test('returns a rejected promise if the given function fails', done => { 14 | const e = new Error(); 15 | mutex 16 | .lock(undefined, () => { 17 | throw e; 18 | }) 19 | .catch(err => { 20 | expect(err).toEqual(e); 21 | done(); 22 | }); 23 | }); 24 | 25 | test('returns a rejected promise if the given argument is not a function', done => { 26 | // $FlowFixMe 27 | mutex.lock(undefined, 3).catch(err => { 28 | expect(err.toString()).toBe('Error: argument not function'); 29 | done(); 30 | }); 31 | }); 32 | 33 | test('allows only one promise chain to run at a time', done => { 34 | const xs = []; 35 | 36 | function task(x) { 37 | xs.push(x); 38 | return Promise.resolve(x); 39 | } 40 | 41 | function chain(x) { 42 | return task(x) 43 | .then(y => { 44 | return task(y); 45 | }) 46 | .then(z => { 47 | return task(z); 48 | }); 49 | } 50 | 51 | function run(x) { 52 | return mutex.lock(undefined, () => { 53 | return chain(x); 54 | }); 55 | } 56 | 57 | Promise.all([run(5), run(8), run(11)]).then(rs => { 58 | expect(rs).toEqual([5, 8, 11]); 59 | expect(xs).toEqual([5, 5, 5, 8, 8, 8, 11, 11, 11]); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('mutex.isLocked(...)', () => { 66 | const mutex = new Mutex(); 67 | it('returns false while being not locked', () => { 68 | expect(mutex.isLocked()).toBeFalsy(); 69 | }); 70 | 71 | it('returns true while being locked', done => { 72 | function task() { 73 | expect(mutex.isLocked()).toBeTruthy(); 74 | return Promise.resolve(null); 75 | } 76 | 77 | function chain() { 78 | return task().then(() => { 79 | return task(); 80 | }); 81 | } 82 | 83 | mutex.lock(undefined, chain).then(() => { 84 | expect(mutex.isLocked()).toBeFalsy(); 85 | done(); 86 | }); 87 | 88 | expect(mutex.isLocked()).toBeTruthy(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /packages/client/__tests__/storage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { InMemory } from '../src/storage'; 3 | 4 | describe('InMemory', () => { 5 | const store = new InMemory(); 6 | 7 | test('initial state', () => { 8 | expect(store.storage).toEqual({}); 9 | }); 10 | 11 | test('set', async () => { 12 | await store.set('foo', 'bar'); 13 | }); 14 | 15 | test('get', async () => { 16 | const foo = await store.get('foo'); 17 | expect(foo).toBe('bar'); 18 | }); 19 | 20 | test('keys', async () => { 21 | const keys = await store.keys(); 22 | expect(keys).toEqual(['foo']); 23 | }); 24 | 25 | test('remove', async () => { 26 | await store.remove('foo'); 27 | const foo2 = await store.get('foo'); 28 | expect(foo2).toBe(null); 29 | }); 30 | 31 | test('merge', async () => { 32 | const merge = (n: number): any => (prev: string | null): string | null => { 33 | return (prev || '') + n; 34 | }; 35 | 36 | const result = await Promise.all([ 37 | store.merge('~', merge(0)), 38 | store.merge('~', merge(1)), 39 | store.merge('~', merge(2)), 40 | store.merge('~', merge(3)), 41 | store.merge('~', merge(4)), 42 | store.merge('~', merge(5)), 43 | ]); 44 | 45 | expect(result).toEqual(['0', '01', '012', '0123', '01234', '012345']); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/client", 3 | "version": "0.1.1", 4 | "description": "Swarm Bare-Bones Client", 5 | "author": "Victor Grishchenko ", 6 | "contributors": [ 7 | "Oleg Lebedev (https://github.com/olebedev)" 8 | ], 9 | "main": "lib/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "peerDependencies": { 15 | "react-native": "^0.53.2" 16 | }, 17 | "dependencies": { 18 | "@swarm/clock": "^0.1.1", 19 | "@swarm/rdt": "^0.1.1", 20 | "@swarm/ron": "^0.1.1", 21 | "@swarm/ron-uuid": "^0.1.1", 22 | "regenerator-runtime": "^0.11.1" 23 | }, 24 | "files": [ 25 | "lib/*.js", 26 | "lib/*.js.flow" 27 | ], 28 | "scripts": { 29 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 30 | "build:clean": "../../node_modules/.bin/rimraf lib", 31 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 32 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 33 | }, 34 | "keywords": [ 35 | "swarm", 36 | "replicated", 37 | "RON", 38 | "CRDT" 39 | ], 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /packages/client/src/asyncStorage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // $FlowFixMe 4 | import { AsyncStorage } from 'react-native'; 5 | import Mutex from './mutex'; 6 | 7 | export default class { 8 | mu: Mutex; 9 | 10 | constructor() { 11 | this.mu = new Mutex(); 12 | } 13 | 14 | merge( 15 | key: string, 16 | reduce: (prev: string | null) => string | null, 17 | ): Promise { 18 | return this.mu.lock(key, async () => { 19 | let prev = await AsyncStorage.getItem(key); 20 | prev = typeof prev === 'undefined' ? null : prev; 21 | const value = reduce(prev); 22 | if (value !== null) { 23 | await AsyncStorage.setItem(key, value); 24 | } else if (prev !== null) { 25 | await AsyncStorage.removeItem(key); 26 | } 27 | return value; 28 | }); 29 | } 30 | 31 | set(key: string, value: string): Promise { 32 | return new Promise((res, rej) => { 33 | AsyncStorage.setItem(key, value, err => { 34 | if (err) return rej(err); 35 | res(); 36 | }); 37 | }); 38 | } 39 | 40 | get(key: string): Promise { 41 | return new Promise((res, rej) => { 42 | AsyncStorage.getItem(key, (err, result) => { 43 | if (err) return rej(err); 44 | res(typeof result === 'undefined' ? null : result); 45 | }); 46 | }); 47 | } 48 | 49 | multiGet(keys: string[]): Promise<{ [string]: string | null }> { 50 | return new Promise((res, rej) => { 51 | AsyncStorage.multiGet(keys, (err, tuples) => { 52 | if (err) return rej(err); 53 | const ret = {}; 54 | tuples.map((result, i, store) => { 55 | const key = store[i][0]; 56 | const value = store[i][1]; 57 | ret[key] = value; 58 | }); 59 | res(ret); 60 | }); 61 | }); 62 | } 63 | 64 | remove(key: string): Promise { 65 | return new Promise((res, rej) => { 66 | AsyncStorage.removeItem(key, err => { 67 | if (err) return rej(err); 68 | res(); 69 | }); 70 | }); 71 | } 72 | 73 | keys(): Promise> { 74 | return new Promise((res, rej) => { 75 | AsyncStorage.getAllKeys((err, keys) => { 76 | if (err) return rej(err); 77 | res(keys || []); 78 | }); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/client/src/connection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import RWS from './rws'; 4 | import Op from '@swarm/ron'; 5 | import { ZERO } from '@swarm/ron-uuid'; 6 | import { Frame } from '@swarm/ron'; 7 | 8 | export interface Connection { 9 | onmessage: (ev: MessageEvent) => any; 10 | onopen: (ev: Event) => any; 11 | send(data: string): void; 12 | readyState: number; 13 | close(): void; 14 | open(): void; 15 | } 16 | 17 | // DevNull connection is used for permanent offline-mode 18 | export class DevNull implements Connection { 19 | onmessage: (ev: MessageEvent) => any; 20 | onopen: (ev: Event) => any; 21 | readyState: number; 22 | constructor() { 23 | this.readyState = 0; 24 | setTimeout(() => { 25 | this.readyState = 0; 26 | this.onopen && this.onopen(new Event('')); 27 | }, 0); 28 | } 29 | 30 | send(data: string): void { 31 | if (!this.onmessage) return; 32 | const frame = new Frame(data); 33 | if (!frame.isPayload()) return; 34 | for (const op of frame) { 35 | if (!op.uuid(2).eq(ZERO)) { 36 | setTimeout(() => { 37 | // $FlowFixMe 38 | this.onmessage({ data: `@${op.uuid(2).toString()}!` }); 39 | }, 0); 40 | return; 41 | } 42 | } 43 | } 44 | 45 | close(): void {} 46 | open(): void {} 47 | } 48 | 49 | export class Verbose extends RWS implements Connection { 50 | _om: (ev: MessageEvent) => any; 51 | _oo: (ev: Event) => any; 52 | 53 | constructor(url: string, protocols: string[] = [], options: {} = {}) { 54 | super(url, protocols, options); 55 | const send = this.send; 56 | this.send = (data: string): void => { 57 | console.log( 58 | '%c(≶) %c%s %c%s', 59 | 'color: blue', 60 | 'color: green;', 61 | '~>', 62 | 'color: #aaa', 63 | data, 64 | ); 65 | send(data); 66 | }; 67 | } 68 | 69 | get onopen(): (ev: Event) => any { 70 | return (ev: Event) => { 71 | console.log( 72 | '%c(≶) %c%s', 73 | 'color: blue;', 74 | 'color: green;', 75 | // $FlowFixMe 76 | 'connected to ' + this._url, 77 | ); 78 | this._oo(ev); 79 | }; 80 | } 81 | 82 | set onopen(m: (ev: Event) => void): void { 83 | this._oo = m; 84 | } 85 | 86 | get onmessage(): (ev: MessageEvent) => any { 87 | return (ev: MessageEvent) => { 88 | console.log( 89 | '%c(≶) %c%s %c%s', 90 | 'color: blue;', 91 | 'color: red;', 92 | '<~', 93 | 'color: #aaa', 94 | ev.data, 95 | ); 96 | this._om(ev); 97 | }; 98 | } 99 | 100 | set onmessage(m: (ev: MessageEvent) => any): void { 101 | this._om = m; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/client/src/mutex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // inspired by https://github.com/plenluno/promise-mutex 4 | 5 | export default class Mutex { 6 | locks: { [string]: true | void }; 7 | 8 | constructor() { 9 | this.locks = {}; 10 | } 11 | 12 | lock(key: string = '~', f: () => Promise | T): Promise { 13 | const executor = (resolve, reject) => { 14 | if (!this._lock(key)) { 15 | setTimeout(() => { 16 | executor(resolve, reject); 17 | }, 0); 18 | return; 19 | } 20 | 21 | if (!(f instanceof Function)) { 22 | reject(new Error('argument not function')); 23 | this._unlock(key); 24 | return; 25 | } 26 | 27 | let r; 28 | try { 29 | r = f(); 30 | } catch (e) { 31 | reject(e); 32 | this._unlock(key); 33 | return; 34 | } 35 | 36 | Promise.resolve(r) 37 | .then(res => { 38 | resolve(res); 39 | this._unlock(key); 40 | }) 41 | .catch(err => { 42 | reject(err); 43 | this._unlock(key); 44 | }); 45 | }; 46 | 47 | return new Promise(executor); 48 | } 49 | 50 | isLocked(key: string = '~'): boolean { 51 | return !!this.locks[key]; 52 | } 53 | 54 | _lock(key: string): boolean { 55 | if (!!this.locks[key]) return false; 56 | return (this.locks[key] = true); 57 | } 58 | 59 | _unlock(key: string): boolean { 60 | if (!this.locks[key]) return false; 61 | delete this.locks[key]; 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/client/src/pending.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import regeneratorRuntime from 'regenerator-runtime'; // for async/await work flow 4 | 5 | import type { Storage } from './storage'; 6 | import Op from '@swarm/ron'; 7 | import UUID, { ZERO } from '@swarm/ron-uuid'; 8 | 9 | export const KEY = '__pending__'; 10 | 11 | // A wrapper for convenience and to work with pending 12 | // ops in an efficient way. 13 | class PendingOps { 14 | storage: Storage; 15 | ops: Array; 16 | _onIdle: void | (() => void); 17 | period: number; 18 | timer: TimeoutID; 19 | seen: UUID; 20 | 21 | constructor(storage: Storage, ops: string[]): PendingOps { 22 | this.storage = storage; 23 | this.ops = ops; 24 | this.seen = ZERO; 25 | return this; 26 | } 27 | 28 | setIdlePeriod(period: number): void { 29 | this.period = period; 30 | this.check(); 31 | } 32 | 33 | onIdle(f?: () => void): void { 34 | this._onIdle = f; 35 | } 36 | 37 | check = (): void => { 38 | clearTimeout(this.timer); 39 | if (this.ops.length && this._onIdle) this._onIdle(); 40 | if (this.period > 0) { 41 | this.timer = setTimeout(this.check, this.period); 42 | } 43 | }; 44 | 45 | /*:: @@iterator(): Iterator { return ({}: any); } */ 46 | 47 | // $FlowFixMe - computed property 48 | [Symbol.iterator](): Iterator { 49 | return this.ops[Symbol.iterator](); 50 | } 51 | 52 | push(frame: string): Promise { 53 | this.ops.push(frame); 54 | return this.flush(); 55 | } 56 | 57 | release(ack: UUID): Promise { 58 | if (this.seen.gt(ack)) return Promise.resolve(); 59 | this.seen = ack; 60 | let i = -1; 61 | for (const _old of this.ops) { 62 | i++; 63 | const old = Op.fromString(_old); 64 | if (!old) throw new Error(`malformed op: '${_old}'`); 65 | 66 | if (old.event.gt(ack)) { 67 | this.ops = this.ops.slice(i + 1); 68 | break; 69 | } 70 | } 71 | if (i === this.ops.length - 1) this.ops = []; 72 | 73 | return this.flush(); 74 | } 75 | 76 | flush(): Promise { 77 | return this.storage.set(KEY, JSON.stringify(this.ops)); 78 | } 79 | 80 | get length(): number { 81 | return this.ops.length; 82 | } 83 | 84 | static async read(storage: Storage): Promise { 85 | const pending = await storage.get(KEY); 86 | return new PendingOps(storage, JSON.parse(pending || '[]')); 87 | } 88 | } 89 | 90 | export default PendingOps; 91 | -------------------------------------------------------------------------------- /packages/client/src/rws.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This code copied from https://github.com/vmakhaev/reconnectable-websocket 3 | * for minor specific changes. 4 | * 5 | * Author Vladimir Makhaev. 6 | */ 7 | 8 | const defaultOptions = { 9 | debug: false, 10 | automaticOpen: true, 11 | reconnectOnError: false, 12 | reconnectInterval: 1000, 13 | maxReconnectInterval: 30000, 14 | reconnectDecay: 1.5, 15 | timeoutInterval: 2000, 16 | maxReconnectAttempts: null, 17 | randomRatio: 3, 18 | binaryType: 'blob', 19 | reconnectOnCleanClose: false, 20 | }; 21 | 22 | class ReconnectableWebSocket { 23 | constructor(url, protocols = [], options = {}) { 24 | this._url = url; 25 | this._protocols = protocols; 26 | this._options = Object.assign({}, defaultOptions, options); 27 | this._reconnectAttempts = 0; 28 | this.readyState = ReconnectableWebSocket.CONNECTING; 29 | this.wasClean = false; 30 | 31 | if (typeof this._options.debug === 'function') { 32 | this._debug = this._options.debug; 33 | } else if (this._options.debug) { 34 | this._debug = console.log.bind(console); 35 | } else { 36 | this._debug = function() {}; 37 | } 38 | 39 | if (this._options.automaticOpen) this.open(); 40 | } 41 | 42 | open = () => { 43 | this.wasClean = false; 44 | let socket = (this._socket = new WebSocket(this._url, this._protocols)); 45 | socket.binaryType = this._options.binaryType; 46 | 47 | if ( 48 | this._options.maxReconnectAttempts && 49 | this._options.maxReconnectAttempts < this._reconnectAttempts 50 | ) { 51 | return; 52 | } 53 | 54 | this._syncState(); 55 | 56 | socket.onmessage = this._onmessage.bind(this); 57 | socket.onopen = this._onopen.bind(this); 58 | socket.onclose = this._onclose.bind(this); 59 | socket.onerror = this._onerror.bind(this); 60 | }; 61 | 62 | send = data => { 63 | if (this._socket && this._socket.readyState === ReconnectableWebSocket.OPEN) { 64 | this._socket.send(data); 65 | } else if (!this._socket || this._socket.readyState > ReconnectableWebSocket.OPEN) { 66 | this._tryReconnect(); 67 | } 68 | }; 69 | 70 | close = (code, reason) => { 71 | this.wasClean = true; 72 | if (typeof code === 'undefined') code = 1000; 73 | if (this._socket && this._socket.readyState === ReconnectableWebSocket.OPEN) { 74 | this._socket.close(code, reason); 75 | } 76 | }; 77 | 78 | _onmessage = message => { 79 | this.onmessage && this.onmessage(message); 80 | }; 81 | 82 | _onopen = event => { 83 | this._syncState(); 84 | this._reconnectAttempts = 0; 85 | 86 | this.onopen && this.onopen(event); 87 | }; 88 | 89 | _onclose = event => { 90 | this._syncState(); 91 | this._debug('WebSocket: connection is broken', event); 92 | 93 | this.onclose && this.onclose(event); 94 | 95 | this._tryReconnect(event); 96 | }; 97 | 98 | _onerror = event => { 99 | // To avoid undetermined state, we close socket on error 100 | this.close(); 101 | 102 | this._debug('WebSocket: error', event); 103 | this._syncState(); 104 | 105 | this.onerror && this.onerror(event); 106 | 107 | if (this._options.reconnectOnError) this._tryReconnect(event); 108 | }; 109 | 110 | _tryReconnect = e => { 111 | if (this.wasClean && !this._options.reconnectOnCleanClose) { 112 | return; 113 | } 114 | setTimeout(() => { 115 | if ( 116 | this.readyState === ReconnectableWebSocket.CLOSING || 117 | this.readyState === ReconnectableWebSocket.CLOSED 118 | ) { 119 | this._reconnectAttempts++; 120 | this.open(); 121 | } 122 | }, this._getTimeout()); 123 | }; 124 | 125 | _getTimeout = () => { 126 | let timeout = 127 | this._options.reconnectInterval * 128 | Math.pow(this._options.reconnectDecay, this._reconnectAttempts); 129 | timeout = 130 | timeout > this._options.maxReconnectInterval ? this._options.maxReconnectInterval : timeout; 131 | return this._options.randomRatio 132 | ? getRandom(timeout / this._options.randomRatio, timeout) 133 | : timeout; 134 | }; 135 | 136 | _syncState = () => { 137 | this.readyState = this._socket.readyState; 138 | }; 139 | } 140 | 141 | function getRandom(min, max) { 142 | return Math.random() * (max - min) + min; 143 | } 144 | 145 | ReconnectableWebSocket.CONNECTING = 0; 146 | ReconnectableWebSocket.OPEN = 1; 147 | ReconnectableWebSocket.CLOSING = 2; 148 | ReconnectableWebSocket.CLOSED = 3; 149 | 150 | export default ReconnectableWebSocket; 151 | -------------------------------------------------------------------------------- /packages/client/src/storage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export interface Storage { 4 | merge( 5 | key: string, 6 | reduce: (prev: string | null) => string | null, 7 | ): Promise; 8 | set(key: string, value: string): Promise; 9 | get(key: string): Promise; 10 | multiGet(keys: string[]): Promise<{ [string]: string | null }>; 11 | remove(key: string): Promise; 12 | keys(): Promise>; 13 | } 14 | 15 | export class InMemory implements Storage { 16 | storage: { [string]: string }; 17 | 18 | constructor(storage: { [string]: string } = {}) { 19 | this.storage = storage; 20 | } 21 | 22 | merge( 23 | key: string, 24 | reduce: (prev: string | null) => string | null, 25 | ): Promise { 26 | const value = reduce( 27 | this.storage.hasOwnProperty(key) ? this.storage[key] : null, 28 | ); 29 | if (value !== null) { 30 | this.storage[key] = value; 31 | } else { 32 | delete this.storage[key]; 33 | } 34 | return Promise.resolve(value); 35 | } 36 | 37 | set(key: string, value: string): Promise { 38 | this.storage[key] = value; 39 | return Promise.resolve(); 40 | } 41 | 42 | get(key: string): Promise { 43 | const v = this.storage[key]; 44 | return Promise.resolve(typeof v === 'undefined' ? null : v); 45 | } 46 | 47 | multiGet(keys: string[]): Promise<{ [string]: string | null }> { 48 | const ret = {}; 49 | for (const k of keys) { 50 | ret[k] = this.storage[k]; 51 | if (typeof ret[k] === 'undefined') ret[k] = null; 52 | } 53 | return Promise.resolve(ret); 54 | } 55 | 56 | remove(key: string): Promise { 57 | delete this.storage[key]; 58 | return Promise.resolve(); 59 | } 60 | 61 | keys(): Promise> { 62 | return Promise.resolve(Object.keys(this.storage)); 63 | } 64 | } 65 | 66 | export class LocalStorage implements Storage { 67 | merge( 68 | key: string, 69 | reduce: (prev: string | null) => string | null, 70 | ): Promise { 71 | // $FlowFixMe 72 | const value = reduce(localStorage.getItem(key)); 73 | if (value !== null) { 74 | localStorage.setItem(key, value); 75 | } else { 76 | localStorage.removeItem(key); 77 | } 78 | return Promise.resolve(value); 79 | } 80 | 81 | set(key: string, value: string): Promise { 82 | localStorage.setItem(key, value); 83 | return Promise.resolve(); 84 | } 85 | 86 | get(key: string): Promise { 87 | const v = localStorage.getItem(key); 88 | return Promise.resolve(typeof v === 'undefined' ? null : v); 89 | } 90 | 91 | multiGet(keys: string[]): Promise<{ [string]: string | null }> { 92 | const ret = {}; 93 | for (const k of keys) { 94 | const item = localStorage.getItem(k); 95 | ret[k] = typeof item === 'undefined' ? null : item; 96 | } 97 | return Promise.resolve(ret); 98 | } 99 | 100 | remove(key: string): Promise { 101 | localStorage.removeItem(key); 102 | return Promise.resolve(); 103 | } 104 | 105 | keys(): Promise> { 106 | return Promise.resolve(Object.keys(localStorage)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/clock/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/clock/README.md: -------------------------------------------------------------------------------- 1 | # Hybrid/logical clock, a UUID factory 2 | 3 | ``` 4 | ``` 5 | -------------------------------------------------------------------------------- /packages/clock/__tests__/clock.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | 4 | import { Logical, Calendar, calendarBase2Date } from '../src'; 5 | import UUID from '../../ron-uuid/src'; 6 | 7 | describe('Logical', () => { 8 | const clock = new Logical('test'); 9 | test('basic', () => { 10 | expect(clock.time().toString()).toBe('(1+test'); 11 | expect(clock.time().toString()).toBe('(2+test'); 12 | expect(clock.time().toString()).toBe('(3+test'); 13 | clock.see(UUID.fromString('(6+test')); 14 | expect(clock.time().toString()).toBe('(7+test'); 15 | 16 | const clock10 = new Logical('orig', { length: 10, last: '(5+other' }); 17 | expect(clock10.time().toString()).toBe('(500001+orig'); 18 | }); 19 | }); 20 | 21 | describe('Calendar', () => { 22 | test('basic', () => { 23 | const clock = new Calendar('orig'); 24 | clock.time(); 25 | expect(clock.time().toString() < clock.time().toString()).toBeTruthy(); 26 | }); 27 | 28 | test('adjust', () => { 29 | const clock = new Calendar('orig', { length: 7, offset: 0 }); 30 | const now = clock.time(); 31 | 32 | clock._offset = 864e5; // one day 33 | const nextDay = clock.time(); 34 | 35 | expect(clock.last().value).toBe(nextDay.value); 36 | clock.adjust(now); 37 | 38 | expect(-100 < clock._offset && clock._offset < 0).toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/clock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/clock", 3 | "version": "0.1.1", 4 | "description": "A simple logical clock generating RON UUIDs", 5 | "author": "Victor Grishchenko ", 6 | "contributors": [ 7 | "Oleg Lebedev (https://github.com/olebedev)" 8 | ], 9 | "main": "lib/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "dependencies": { 15 | "@swarm/ron-uuid": "^0.1.1" 16 | }, 17 | "files": [ 18 | "lib/*.js", 19 | "lib/*.js.flow" 20 | ], 21 | "scripts": { 22 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 23 | "build:clean": "../../node_modules/.bin/rimraf lib", 24 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 25 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 26 | }, 27 | "keywords": [ 28 | "swarm", 29 | "replicated", 30 | "ron", 31 | "logical clock", 32 | "protocol" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /packages/clock/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | 4 | import UUID, { ERROR, ZERO, BASE64, CODES } from '@swarm/ron-uuid'; 5 | 6 | export interface Clock { 7 | time(): UUID; 8 | last(): UUID; 9 | see(UUID): boolean; 10 | origin(): string; 11 | adjust(UUID | number): number; 12 | isSane(UUID): boolean; 13 | } 14 | 15 | // Pure logical clock. 16 | export class Logical implements Clock { 17 | _origin: string; 18 | _last: UUID; 19 | length: number; 20 | 21 | // Create a new clock. 22 | constructor( 23 | origin: string, 24 | options: { length?: number, last?: UUID | string } = {}, 25 | ): Logical { 26 | this._origin = origin; 27 | this._last = ZERO; 28 | this.length = 5; 29 | if (options) { 30 | if (options.length) this.length = options.length; 31 | if (options.last) this._last = UUID.fromString(options.last.toString()); 32 | } 33 | return this; 34 | } 35 | 36 | // Generates a fresh globally unique monotonous UUID. 37 | time(): UUID { 38 | let t = this._last.value; 39 | while (t.length < this.length) t += '0'; 40 | let i = t.length - 1; 41 | while (t[i] === '~' && i >= 0) i--; 42 | if (i < 0) return ERROR; 43 | const value = t.substr(0, i) + BASE64[CODES[t.charCodeAt(i)] + 1]; 44 | this._last = new UUID(value, this._origin, '+'); 45 | return this._last; 46 | } 47 | 48 | // See an UUID. Can only generate larger UUIDs afterwards. 49 | see(uuid: UUID) { 50 | if (this.isSane(uuid) && this._last.lt(uuid)) { 51 | this._last = uuid; 52 | return true; 53 | } 54 | return false; 55 | } 56 | 57 | origin(): string { 58 | return this._origin; 59 | } 60 | 61 | last(): UUID { 62 | return this._last; 63 | } 64 | 65 | adjust(event: UUID | number): number { 66 | if (event instanceof UUID) { 67 | this.see(event); 68 | } 69 | return 0; 70 | } 71 | 72 | isSane(event: UUID): boolean { 73 | return !!event.value && event.value < `~`; 74 | } 75 | } 76 | 77 | export class Calendar implements Clock { 78 | _last: UUID; 79 | _lastPair: Pair; 80 | _lastBase: string; 81 | _origin: string; 82 | _offset: number; 83 | _minlen: number; 84 | 85 | constructor( 86 | origin: string, 87 | options: { last?: UUID | string, offset?: number, length?: number } = {}, 88 | ): Calendar { 89 | this._offset = options.offset || 0; 90 | this._origin = origin; 91 | this._last = options.last 92 | ? // $FlowFixMe 93 | new UUID(options.last.value, origin, '+') 94 | : ZERO; 95 | this._lastPair = { high: -1, low: -1 }; 96 | this._lastBase = '0'; 97 | this._minlen = options.length || 6; 98 | return this; 99 | } 100 | 101 | time(): UUID { 102 | let pair = date2pair(new Date(Date.now() + this._offset)); 103 | let next = pair2base(pair); 104 | 105 | if ( 106 | pair.high <= this._lastPair.high || 107 | (pair.high === this._lastPair.high && pair.low <= this._lastPair.low) 108 | ) { 109 | pair = further(pair, this._lastPair); 110 | next = pair2base(pair); 111 | } else if (this._minlen < 8) { 112 | next = relax(next, this._lastBase, this._minlen); 113 | } 114 | 115 | this._lastBase = next; 116 | this._lastPair = pair; 117 | this._last = new UUID(this._lastBase, this._origin, '+'); 118 | return this._last; 119 | } 120 | 121 | see(uuid: UUID): boolean { 122 | if (this.isSane(uuid) && this._last.lt(uuid)) { 123 | this._last = uuid; 124 | this._lastBase = uuid.value; 125 | this._lastPair = base2pair(this._lastBase); 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | origin(): string { 132 | return this._origin; 133 | } 134 | 135 | last(): UUID { 136 | return this._last; 137 | } 138 | 139 | adjust(event: UUID | number): number { 140 | if (event instanceof UUID) { 141 | const { value } = event; 142 | this._offset = calendarBase2Date(value).getTime() - Date.now(); 143 | this._last = new UUID(value, this._origin, '+'); 144 | this._lastPair = base2pair(value); 145 | this._lastBase = value; 146 | } else { 147 | this._offset = event - Date.now(); 148 | const d = new Date(event); 149 | this._lastPair = date2pair(d); 150 | this._lastBase = pair2base(this._lastPair); 151 | this._last = new UUID(this._lastBase, this._origin, '+'); 152 | } 153 | return this._offset; 154 | } 155 | 156 | isSane(event: UUID): boolean { 157 | return !!event.value && event.value < `~`; 158 | } 159 | } 160 | 161 | export function calendarBase2Date(base: string): Date { 162 | return pair2date(base2pair(base)); 163 | } 164 | 165 | type Pair = {| high: number, low: number |}; 166 | 167 | function date2pair(d: Date): Pair { 168 | var high = (d.getUTCFullYear() - 2010) * 12 + d.getUTCMonth(); 169 | high <<= 6; 170 | high |= d.getUTCDate() - 1; 171 | high <<= 6; 172 | high |= d.getUTCHours(); 173 | high <<= 6; 174 | high |= d.getUTCMinutes(); 175 | var low = d.getUTCSeconds(); 176 | low <<= 12; 177 | low |= d.getUTCMilliseconds(); 178 | low <<= 12; 179 | return { high, low }; 180 | } 181 | 182 | function pair2date(pair: Pair): Date { 183 | let { low, high } = pair; 184 | low >>= 12; 185 | let msec = low & 4095; 186 | low >>= 12; 187 | let second = low & 63; 188 | let minute = high & 63; 189 | high >>= 6; 190 | let hour = high & 63; 191 | high >>= 6; 192 | let day = (high & 63) + 1; 193 | high >>= 6; 194 | let months = high & 4095; 195 | let month = months % 12; 196 | let year = 2010 + (((months - month) / 12) | 0); 197 | let ms = Date.UTC(year, month, day, hour, minute, second, msec); 198 | return new Date(ms); 199 | } 200 | 201 | function base2pair(base: string): Pair { 202 | const high = base64x32toInt(base.substr(0, 5)); 203 | const low = base.length <= 5 ? 0 : base64x32toInt(base.substr(5, 5)); 204 | return { high, low }; 205 | } 206 | 207 | function pair2base(pair: Pair): string { 208 | var ret = intToBase64x32(pair.high, pair.low !== 0); 209 | if (pair.low === 0) { 210 | if (ret === '') { 211 | ret = '0'; 212 | } 213 | } else { 214 | ret += intToBase64x32(pair.low, false); 215 | } 216 | return ret; 217 | } 218 | 219 | /** convert int to a Base64x32 number (right zeroes skipped) */ 220 | function intToBase64x32(i: number, pad: boolean) { 221 | if (i < 0 || i >= 1 << 30) { 222 | throw new Error('out of range: ' + i); 223 | } 224 | var ret = '', 225 | pos = 0; 226 | while (!pad && (i & 63) === 0 && pos++ < 5) { 227 | i >>= 6; 228 | } 229 | while (pos++ < 5) { 230 | ret = BASE64.charAt(i & 63) + ret; 231 | i >>= 6; 232 | } 233 | return ret; 234 | } 235 | 236 | function base64x32toInt(base: string): number { 237 | if (base.length > 5) { 238 | throw new Error('more than 30 bits'); 239 | } 240 | var ret = 0, 241 | i = 0; 242 | while (i < base.length) { 243 | ret <<= 6; 244 | var code = base.charCodeAt(i); 245 | if (code >= 128) { 246 | throw new Error('invalid char'); 247 | } 248 | var de = CODES[code]; 249 | if (de === -1) { 250 | throw new Error('non-base64 char'); 251 | } 252 | ret |= de; 253 | i++; 254 | } 255 | while (i++ < 5) { 256 | ret <<= 6; 257 | } 258 | return ret; 259 | } 260 | 261 | const MAX32 = (1 << 30) - 1; 262 | 263 | function further(pair: Pair, prev: Pair): Pair { 264 | if (pair.low < MAX32) { 265 | return { 266 | high: Math.max(pair.high, prev.high), 267 | low: Math.max(pair.low, prev.low) + 1, 268 | }; 269 | } else { 270 | return { high: Math.max(pair.high, prev.high) + 1, low: 0 }; 271 | } 272 | } 273 | 274 | function relax(next: string, prev: string, minLength: number = 1): string { 275 | const reper = toFullString(prev); 276 | const mine = toFullString(next); 277 | let p = 0; 278 | while (p < 10 && mine[p] === reper[p]) p++; 279 | p++; 280 | if (p < minLength) p = minLength; 281 | return mine.substr(0, p); 282 | } 283 | 284 | function toFullString(base: string): string { 285 | return base + '0000000000'.substr(base.length); 286 | } 287 | -------------------------------------------------------------------------------- /packages/db/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/db/__tests__/basic.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import gql from 'graphql-tag'; 4 | import { Frame, UUID } from '../../ron/src'; 5 | import { Connection } from '../../__tests__/fixtures'; 6 | import SwarmDB from '../src'; 7 | import type { Response } from '../src'; 8 | import { InMemory } from '../../client/src/storage'; 9 | 10 | test('swarm.execute({ subscription })', async () => { 11 | const storage = new InMemory(); 12 | let swarm = new SwarmDB({ 13 | storage, 14 | upstream: new Connection('014-gql-subs.ron'), 15 | db: { 16 | id: 'user', 17 | name: 'test', 18 | auth: 'JwT.t0k.en', 19 | clockMode: 'Logical', 20 | }, 21 | }); 22 | 23 | await swarm.ensure(); 24 | 25 | const objID = swarm.uuid(); 26 | const listID = swarm.uuid(); 27 | 28 | await swarm.set(objID, { 29 | a: 42, 30 | b: 'wat', 31 | c: 0.1, 32 | d: false, 33 | e: true, 34 | f: listID, 35 | }); 36 | 37 | const list = [ 38 | swarm.uuid(), 39 | swarm.uuid(), 40 | swarm.uuid(), 41 | swarm.uuid(), 42 | swarm.uuid(), 43 | swarm.uuid(), 44 | swarm.uuid().local(), 45 | swarm.uuid(), 46 | swarm.uuid(), 47 | swarm.uuid(), 48 | ]; 49 | 50 | let c = 1; 51 | for (const item of list) { 52 | await swarm.add(listID, item); 53 | await swarm.set(item, { value: c++ }); 54 | } 55 | 56 | expect(storage.storage).toEqual({ 57 | '1ABC1+user': "*lww#1ABC1+user@1ABC3+user!:a=42:b'wat':c^0.1:d>false:e>true:f>1ABC2+user", 58 | '1ABC2+user': 59 | '*set#1ABC2+user@1ABCW+user!>1ABCD+user@(U+>1ABCC+user@(S+>1ABCB+user@(O+>1ABC9+user@(M+>1ABC8+user@(K+>1ABC7+user@(I+>1ABC6+user@(G+>1ABC5+user@(E+>1ABC4+user', 60 | '1ABC4+user': '*lww#1ABC4+user@1ABCF+user!:value=1', 61 | '1ABC5+user': '*lww#1ABC5+user@1ABCH+user!:value=2', 62 | '1ABC6+user': '*lww#1ABC6+user@1ABCJ+user!:value=3', 63 | '1ABC7+user': '*lww#1ABC7+user@1ABCL+user!:value=4', 64 | '1ABC8+user': '*lww#1ABC8+user@1ABCN+user!:value=5', 65 | '1ABC9+user': '*lww#1ABC9+user@1ABCP+user!:value=6', 66 | '1ABCA+~local': '*lww#1ABCA+~local@1ABCR+user!:value=7', 67 | '1ABCB+user': '*lww#1ABCB+user@1ABCT+user!:value=8', 68 | '1ABCC+user': '*lww#1ABCC+user@1ABCV+user!:value=9', 69 | '1ABCD+user': '*lww#1ABCD+user@1ABCX+user!:value=10', 70 | __meta__: 71 | '{"name":"test","clockLen":5,"forkMode":"// FIXME","peerIdBits":30,"horizont":604800,"offset":0,"id":"user","auth":"JwT.t0k.en","clockMode":"Logical"}', 72 | __pending__: 73 | '["*lww#1ABC1+user@1ABC3+user!:a=42:b\'wat\':c^0.1:d>false:e>true:f>1ABC2+user","*set#1ABC2+user@1ABCE+user!>1ABC4+user","*lww#1ABC4+user@1ABCF+user!:value=1","*set#1ABC2+user@1ABCG+user!>1ABC5+user","*lww#1ABC5+user@1ABCH+user!:value=2","*set#1ABC2+user@1ABCI+user!>1ABC6+user","*lww#1ABC6+user@1ABCJ+user!:value=3","*set#1ABC2+user@1ABCK+user!>1ABC7+user","*lww#1ABC7+user@1ABCL+user!:value=4","*set#1ABC2+user@1ABCM+user!>1ABC8+user","*lww#1ABC8+user@1ABCN+user!:value=5","*set#1ABC2+user@1ABCO+user!>1ABC9+user","*lww#1ABC9+user@1ABCP+user!:value=6","*set#1ABC2+user@1ABCS+user!>1ABCB+user","*lww#1ABCB+user@1ABCT+user!:value=8","*set#1ABC2+user@1ABCU+user!>1ABCC+user","*lww#1ABCC+user@1ABCV+user!:value=9","*set#1ABC2+user@1ABCW+user!>1ABCD+user","*lww#1ABCD+user@1ABCX+user!:value=10"]', 74 | }); 75 | 76 | const q = gql` 77 | subscription Test($id: UUID!, $nope: UUID!) { 78 | result @node(id: $id) { 79 | id 80 | type 81 | a 82 | b 83 | c 84 | d 85 | e 86 | f { 87 | id 88 | type 89 | length 90 | list: id @node @slice(begin: 2, end: 7) { 91 | id 92 | type 93 | value 94 | } 95 | } 96 | internal @node(id: $id) { 97 | a 98 | c 99 | e 100 | flat @node(id: $id) 101 | notExists @node(id: $nope) @weak { 102 | id 103 | test 104 | } 105 | } 106 | } 107 | } 108 | `; 109 | 110 | let res = {}; 111 | let calls = 0; 112 | // $FlowFixMe 113 | const r = await swarm.execute( 114 | { query: q, variables: { id: objID, nope: UUID.fromString('nope') } }, 115 | (v: Response) => { 116 | res = v; 117 | calls++; 118 | }, 119 | ); 120 | 121 | expect(r.ok).toBeTruthy(); 122 | 123 | // waiting for all subscriptions will be initialized 124 | await new Promise(r => setTimeout(r, 100)); 125 | 126 | // console.log(swarm.client.lstn['nope']); 127 | // expect(swarm.client.lstn['nope']).toHaveLength(1); 128 | 129 | expect(res.error).toBeUndefined(); 130 | expect(res.data).toEqual({ 131 | result: { 132 | type: 'lww', 133 | a: 42, 134 | b: 'wat', 135 | c: 0.1, 136 | d: false, 137 | e: true, 138 | f: { 139 | id: '1ABC2+user', 140 | type: 'set', 141 | length: 9, 142 | list: [ 143 | { type: 'lww', id: '1ABCB+user', value: 8 }, 144 | { type: 'lww', id: '1ABC9+user', value: 6 }, 145 | { type: 'lww', id: '1ABC8+user', value: 5 }, 146 | { type: 'lww', id: '1ABC7+user', value: 4 }, 147 | { type: 'lww', id: '1ABC6+user', value: 3 }, 148 | ], 149 | }, 150 | id: '1ABC1+user', 151 | internal: { 152 | a: 42, 153 | c: 0.1, 154 | e: true, 155 | flat: UUID.fromString('1ABC1+user'), 156 | notExists: null, 157 | }, 158 | }, 159 | }); 160 | 161 | expect(calls).toBe(1); 162 | 163 | let item = swarm.uuid(); 164 | await swarm.add(listID, item); 165 | await swarm.set(item, { value: c++ }); 166 | 167 | await new Promise(r => setTimeout(r, 200)); 168 | 169 | expect(calls).toBe(2); 170 | 171 | expect(res.error).toBeUndefined(); 172 | expect(res.data).toEqual({ 173 | result: { 174 | type: 'lww', 175 | a: 42, 176 | b: 'wat', 177 | c: 0.1, 178 | d: false, 179 | e: true, 180 | f: { 181 | id: '1ABC2+user', 182 | type: 'set', 183 | length: 10, 184 | list: [ 185 | { type: 'lww', id: '1ABCC+user', value: 9 }, 186 | { type: 'lww', id: '1ABCB+user', value: 8 }, 187 | { type: 'lww', id: '1ABC9+user', value: 6 }, 188 | { type: 'lww', id: '1ABC8+user', value: 5 }, 189 | { type: 'lww', id: '1ABC7+user', value: 4 }, 190 | ], 191 | }, 192 | id: '1ABC1+user', 193 | internal: { 194 | a: 42, 195 | c: 0.1, 196 | e: true, 197 | flat: UUID.fromString('1ABC1+user'), 198 | notExists: null, 199 | }, 200 | }, 201 | }); 202 | 203 | let ok2 = await swarm.set('nope', { test: 1 }); 204 | expect(ok2).toBeTruthy(); 205 | 206 | await new Promise(r => setTimeout(r, 200)); 207 | 208 | expect(calls).toBe(3); 209 | 210 | // $FlowFixMe 211 | expect(swarm.client.storage.storage['nope']).toBe('*lww#nope@1ABCa+user!:test=1'); 212 | 213 | expect(swarm.cache['nope']).toEqual({ test: 1 }); 214 | 215 | expect(res.error).toBeUndefined(); 216 | expect(res.data).toEqual({ 217 | result: { 218 | type: 'lww', 219 | a: 42, 220 | b: 'wat', 221 | c: 0.1, 222 | d: false, 223 | e: true, 224 | f: { 225 | id: '1ABC2+user', 226 | type: 'set', 227 | length: 10, 228 | list: [ 229 | { type: 'lww', id: '1ABCC+user', value: 9 }, 230 | { type: 'lww', id: '1ABCB+user', value: 8 }, 231 | { type: 'lww', id: '1ABC9+user', value: 6 }, 232 | { type: 'lww', id: '1ABC8+user', value: 5 }, 233 | { type: 'lww', id: '1ABC7+user', value: 4 }, 234 | ], 235 | }, 236 | id: '1ABC1+user', 237 | internal: { 238 | a: 42, 239 | c: 0.1, 240 | e: true, 241 | flat: UUID.fromString('1ABC1+user'), 242 | notExists: { id: 'nope', test: 1 }, 243 | }, 244 | }, 245 | }); 246 | 247 | expect(swarm.subs).toHaveLength(1); 248 | expect(r.off).toBeDefined(); 249 | expect(r.off && r.off()).toBeTruthy(); 250 | expect(swarm.subs).toHaveLength(0); 251 | 252 | ok2 = await swarm.set('nope', { test: 2 }); 253 | expect(ok2).toBeTruthy(); 254 | expect(res.error).toBeUndefined(); 255 | expect(res.data).toEqual({ 256 | result: { 257 | type: 'lww', 258 | a: 42, 259 | b: 'wat', 260 | c: 0.1, 261 | d: false, 262 | e: true, 263 | f: { 264 | id: '1ABC2+user', 265 | type: 'set', 266 | length: 10, 267 | list: [ 268 | { type: 'lww', id: '1ABCC+user', value: 9 }, 269 | { type: 'lww', id: '1ABCB+user', value: 8 }, 270 | { type: 'lww', id: '1ABC9+user', value: 6 }, 271 | { type: 'lww', id: '1ABC8+user', value: 5 }, 272 | { type: 'lww', id: '1ABC7+user', value: 4 }, 273 | ], 274 | }, 275 | id: '1ABC1+user', 276 | internal: { 277 | a: 42, 278 | c: 0.1, 279 | e: true, 280 | flat: UUID.fromString('1ABC1+user'), 281 | notExists: { id: 'nope', test: 1 }, 282 | }, 283 | }, 284 | }); 285 | 286 | await new Promise(r => setTimeout(r, 1000)); 287 | 288 | // $FlowFixMe 289 | let dump = swarm.client.upstream.dump(); 290 | expect(dump.session).toEqual(dump.fixtures); 291 | }); 292 | 293 | test('swarm.execute({ query })', async () => { 294 | const storage = new InMemory(); 295 | const upstream = new Connection('016-gql-query.ron'); 296 | let swarm = new SwarmDB({ 297 | storage, 298 | upstream, 299 | db: { 300 | id: 'user', 301 | name: 'test', 302 | auth: 'JwT.t0k.en', 303 | clockMode: 'Logical', 304 | }, 305 | }); 306 | 307 | swarm = ((swarm: any): SwarmDB); 308 | 309 | await swarm.ensure(); 310 | 311 | const objID = swarm.uuid(); 312 | const listID = swarm.uuid(); 313 | 314 | await swarm.set(objID, { 315 | a: 42, 316 | b: 'wat', 317 | c: 0.1, 318 | d: false, 319 | e: true, 320 | f: listID, 321 | }); 322 | 323 | const id = swarm.uuid(); 324 | 325 | await swarm.add(listID, id); 326 | await swarm.set(id, { value: 1 }); 327 | 328 | const q = gql` 329 | query Test($id: UUID!, $nope: UUID!) { 330 | result @node(id: $id) { 331 | id 332 | type 333 | a 334 | b 335 | c 336 | d 337 | e 338 | f @slice(begin: 2, end: 7) { 339 | id 340 | type 341 | value 342 | } 343 | internal @node(id: $id) { 344 | a 345 | c 346 | e 347 | flat @node(id: $id) 348 | notExists @node(id: $nope) @weak { 349 | id 350 | test 351 | } 352 | } 353 | } 354 | } 355 | `; 356 | 357 | expect(swarm.cache).toEqual({}); 358 | 359 | const res = await new Promise(async resolve => { 360 | const r = await swarm.execute( 361 | { query: q, variables: { id: objID, nope: UUID.fromString('nope') } }, 362 | resolve, 363 | ); 364 | expect(r.ok).toBeTruthy(); 365 | }); 366 | expect(swarm.subs).toHaveLength(0); 367 | 368 | expect(res.error).toBeUndefined(); 369 | expect(res.data).toEqual({ 370 | result: { 371 | type: 'lww', 372 | a: 42, 373 | b: 'wat', 374 | c: 0.1, 375 | d: false, 376 | e: true, 377 | f: [], 378 | id: '1ABC1+user', 379 | internal: { 380 | a: 42, 381 | c: 0.1, 382 | e: true, 383 | flat: UUID.fromString('1ABC1+user'), 384 | notExists: null, 385 | }, 386 | }, 387 | }); 388 | }); 389 | 390 | test('swarm.execute({ mutation })', async () => { 391 | const upstream = new Connection('015-gql-mutation.ron'); 392 | const storage = new InMemory(); 393 | let swarm = new SwarmDB({ 394 | storage, 395 | upstream, 396 | db: { id: 'user', name: 'test', auth: 'JwT.t0k.en', clockMode: 'Logical' }, 397 | }); 398 | 399 | swarm = ((swarm: any): SwarmDB); 400 | 401 | await swarm.ensure(); 402 | 403 | const objID = swarm.uuid(); 404 | 405 | const q = gql` 406 | mutation Test($id: UUID!, $payload: Payload!, $payload2: Payload!) { 407 | set(id: $id, payload: $payload) 408 | another: set(id: $id, payload: $payload2) 409 | } 410 | `; 411 | 412 | let sub; 413 | const resp = await new Promise(async r => { 414 | const payload = { test: 1 }; 415 | const payload2 = { hello: 'world' }; 416 | sub = await swarm.execute({ query: q, variables: { id: objID, payload, payload2 } }, r); 417 | }); 418 | 419 | expect(resp).toEqual({ 420 | data: { 421 | set: true, 422 | another: true, 423 | }, 424 | }); 425 | 426 | await new Promise(r => setTimeout(r, 300)); 427 | const dump = upstream.dump(); 428 | expect(dump.session).toEqual(dump.fixtures); 429 | }); 430 | 431 | test('swarm.execute({ empty })', async () => { 432 | const storage = new InMemory(); 433 | let swarm = new SwarmDB({ 434 | storage, 435 | upstream: new Connection('021-gql-empty-ack.ron'), 436 | db: { id: 'user', name: 'test', auth: 'JwT.t0k.en', clockMode: 'Logical' }, 437 | }); 438 | 439 | await swarm.ensure(); 440 | 441 | const q = gql` 442 | subscription { 443 | result @node(id: "ack") @weak { 444 | id 445 | type 446 | version 447 | } 448 | } 449 | `; 450 | 451 | const cumul = []; 452 | let calls = 0; 453 | 454 | // $FlowFixMe 455 | await new Promise(r => { 456 | swarm.execute({ query: q }, (v: Response) => { 457 | calls++; 458 | cumul.push(v.data); 459 | if (calls === 2) r(); 460 | }); 461 | }); 462 | 463 | expect(calls).toBe(2); 464 | expect(cumul).toEqual([{ result: null }, { result: { id: 'ack', type: '', version: '0' } }]); 465 | }); 466 | -------------------------------------------------------------------------------- /packages/db/__tests__/deps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Dependencies } from '../src/deps'; 4 | 5 | describe('Dependencies', () => { 6 | const deps = new Dependencies(); 7 | 8 | test('put', () => { 9 | deps.put(0, '1ABC3+'); 10 | deps.put(1, '1ABC4+'); // check override 11 | deps.put(2, '1ABC4+'); 12 | deps.put(3, '1ABC5+'); 13 | 14 | expect(deps.index).toEqual({ 15 | '1ABC3+': 0, 16 | '1ABC4+': 2, 17 | '1ABC5+': 3, 18 | }); 19 | 20 | expect(deps.deps[0]).toEqual({ 21 | '1ABC3+': true, 22 | }); 23 | expect(deps.deps[2]).toEqual({ 24 | '1ABC4+': true, 25 | }); 26 | expect(deps.deps[3]).toEqual({ 27 | '1ABC5+': true, 28 | }); 29 | }); 30 | 31 | test('toString', () => { 32 | expect(deps.toString(0)).toBe('#1ABC3+'); 33 | expect(deps.toString(2)).toBe('#1ABC4+'); 34 | expect(deps.toString(3)).toBe('#1ABC5+'); 35 | expect(deps.toString()).toBe('#1ABC3+#(4+#(5+'); 36 | }); 37 | 38 | test('options', () => { 39 | expect(deps.options(0)).toEqual({ 40 | ensure: true, 41 | }); 42 | expect(deps.options(1)).toBeUndefined(); 43 | expect(deps.options(2)).toEqual({ 44 | once: true, 45 | ensure: true, 46 | }); 47 | expect(deps.options(3)).toEqual({ 48 | once: true, 49 | }); 50 | }); 51 | 52 | test('diff', () => { 53 | const from = new Dependencies(); 54 | from.put(0, '1ABC5+'); 55 | from.put(1, '1ABC3+'); 56 | from.put(1, '1ABC4+'); 57 | from.put(3, '1ABC5+'); 58 | 59 | const diff = deps.diff(from); 60 | 61 | expect(diff.index).toEqual({ 62 | '1ABC3+': 0, 63 | '1ABC4+': 2, 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/db/__tests__/partialReactivity.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { Connection } from '../../__tests__/fixtures'; 4 | import SwarmDB from '../src'; 5 | import type { Response } from '../src'; 6 | import { InMemory } from '../../client/src/storage'; 7 | 8 | describe('Partial reactivity', () => { 9 | test('@static', async () => { 10 | const storage = new InMemory(); 11 | let swarm = new SwarmDB({ 12 | storage, 13 | db: { 14 | id: 'user', 15 | name: 'test', 16 | auth: 'JwT.t0k.en', 17 | clockMode: 'Logical', 18 | }, 19 | }); 20 | await swarm.ensure(); 21 | const cumul = []; 22 | // $FlowFixMe 23 | swarm.client.upstream.send = data => { 24 | cumul.push(data); 25 | }; 26 | let query = gql` 27 | subscription Test { 28 | result @node(id: "object") { 29 | id 30 | } 31 | } 32 | `; 33 | const re = await swarm.execute({ query }, v => { 34 | cumul.push(v.data); 35 | }); 36 | 37 | await swarm.client.onMessage('*lww#object@1ABC1+user!:test=5'); 38 | await new Promise(r => setTimeout(r, 10)); 39 | 40 | re.off && re.off(); 41 | 42 | expect(cumul).toEqual([ 43 | '#object?!', 44 | { result: { id: 'object' } }, 45 | '@~?#object,', 46 | ]); 47 | 48 | query = gql` 49 | subscription Test { 50 | result @node(id: "object") @static { 51 | id 52 | } 53 | } 54 | `; 55 | await swarm.execute({ query }, v => { 56 | cumul.push(v.data); 57 | }); 58 | 59 | await new Promise(r => setTimeout(r, 10)); 60 | 61 | expect(cumul).toEqual([ 62 | '#object?!', 63 | { result: { id: 'object' } }, 64 | '@~?#object,', 65 | { result: { id: 'object' } }, 66 | ]); 67 | 68 | query = gql` 69 | query Test { 70 | result @node(id: "object") { 71 | id 72 | } 73 | } 74 | `; 75 | 76 | await swarm.execute({ query }, v => { 77 | cumul.push(v.data); 78 | }); 79 | 80 | await new Promise(r => setTimeout(r, 10)); 81 | 82 | expect(cumul).toEqual([ 83 | '#object?!', 84 | { result: { id: 'object' } }, 85 | '@~?#object,', 86 | { result: { id: 'object' } }, 87 | { result: { id: 'object' } }, 88 | ]); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /packages/db/__tests__/weak.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { Frame, UUID } from '../../ron/src'; 4 | import { Connection } from '../../__tests__/fixtures'; 5 | import SwarmDB from '../src'; 6 | import type { Response } from '../src'; 7 | import { InMemory } from '../../client/src/storage'; 8 | 9 | test('directive @weak', async () => { 10 | const storage = new InMemory(); 11 | const upstream = new Connection('017-ensure-directive.ron'); 12 | let swarm = new SwarmDB({ 13 | storage, 14 | upstream, 15 | db: { id: 'user', name: 'test', auth: 'JwT.t0k.en', clockMode: 'Logical' }, 16 | }); 17 | 18 | await swarm.ensure(); 19 | 20 | const objID = swarm.uuid(); 21 | const listID = swarm.uuid(); 22 | 23 | await swarm.set(objID, { 24 | collection: listID, 25 | }); 26 | 27 | const id = swarm.uuid(); 28 | 29 | await swarm.add(listID, id); 30 | await swarm.set(id, { value: 1 }); 31 | 32 | const q = gql` 33 | query Test($id: UUID!) { 34 | result @node(id: $id) { 35 | id 36 | version 37 | collection { 38 | id 39 | type 40 | version 41 | length 42 | } 43 | notExists @node(id: "nope") @weak { 44 | id 45 | } 46 | } 47 | } 48 | `; 49 | 50 | let c = 0; 51 | setTimeout(() => { 52 | swarm.set('nope', { hello: 'world' }); 53 | }, 1000); 54 | const res = await new Promise(async resolve => { 55 | const r = await swarm.execute({ query: q, variables: { id: objID } }, v => { 56 | c++; 57 | resolve(v); 58 | }); 59 | expect(r.ok).toBeTruthy(); 60 | }); 61 | 62 | expect(res.error).toBeUndefined(); 63 | expect(res.data).toEqual({ 64 | result: { 65 | id: '1ABC1+user', 66 | version: '1ABC3+user', 67 | collection: { 68 | id: '1ABC2+user', 69 | type: 'set', 70 | version: '1ABC5+user', 71 | length: 1, 72 | }, 73 | notExists: null, 74 | }, 75 | }); 76 | 77 | expect(c).toBe(1); 78 | }); 79 | -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/db", 3 | "version": "0.1.2", 4 | "description": "SwarmDB", 5 | "author": "Oleg Lebedev (https://github.com/olebedev)", 6 | "main": "lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "" 10 | }, 11 | "dependencies": { 12 | "@swarm/api": "^0.1.1", 13 | "@swarm/clock": "^0.1.1", 14 | "@swarm/rdt": "^0.1.1", 15 | "@swarm/ron": "^0.1.1", 16 | "@swarm/ron-uuid": "^0.1.1", 17 | "graphql": "^0.13.1", 18 | "graphql-anywhere": "^4.1.5", 19 | "graphql-tag": "^2.8.0", 20 | "object-hash": "^1.2.0" 21 | }, 22 | "files": [ 23 | "lib/*.js", 24 | "lib/*.js.flow" 25 | ], 26 | "scripts": { 27 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 28 | "build:clean": "../../node_modules/.bin/rimraf lib", 29 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 30 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 31 | }, 32 | "keywords": [ 33 | "swarm", 34 | "replicated", 35 | "RON", 36 | "CRDT" 37 | ], 38 | "publishConfig": { 39 | "access": "public" 40 | }, 41 | "license": "MIT" 42 | } 43 | -------------------------------------------------------------------------------- /packages/db/src/deps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Frame } from '@swarm/ron'; 4 | import UUID, { ZERO } from '@swarm/ron-uuid'; 5 | 6 | type Kind = 0 | 1 | 2 | 3; 7 | 8 | export const REACTIVE: Kind = 0; 9 | export const REACTIVE_WEAK: Kind = 1; 10 | export const STATIC: Kind = 2; 11 | export const STATIC_WEAK: Kind = 3; 12 | export const KINDS = [REACTIVE, REACTIVE_WEAK, STATIC, STATIC_WEAK]; 13 | 14 | type depsMap = { [string]: true }; 15 | 16 | export class Dependencies { 17 | deps: Array; 18 | index: { [string]: Kind }; 19 | 20 | constructor() { 21 | this.deps = [{}, {}, {}, {}]; 22 | this.index = {}; 23 | } 24 | 25 | diff(from: Dependencies): Dependencies { 26 | const diff = new Dependencies(); 27 | for (const key of Object.keys(this.index)) { 28 | if ( 29 | !from.index.hasOwnProperty(key) || 30 | from.index[key] !== this.index[key] 31 | ) { 32 | diff.put(this.index[key], key); 33 | } 34 | } 35 | return diff; 36 | } 37 | 38 | toString(kind?: Kind): string { 39 | const ret = Object.keys( 40 | typeof kind !== 'undefined' ? this.deps[kind] : this.index, 41 | ); 42 | if (!ret.length) return ''; 43 | 44 | let res = ''; 45 | ret.map(i => UUID.fromString(i)).reduce((prev, current) => { 46 | res += '#'; 47 | res += current.toString(prev); 48 | return current; 49 | }, ZERO); 50 | return res; 51 | } 52 | 53 | options(kind: Kind): { once?: true, ensure?: true } | void { 54 | switch (kind) { 55 | case REACTIVE: 56 | return { ensure: true }; 57 | case REACTIVE_WEAK: 58 | return; 59 | case STATIC: 60 | return { once: true, ensure: true }; 61 | case STATIC_WEAK: 62 | return { once: true }; 63 | } 64 | } 65 | 66 | put(k: Kind, id: string): void { 67 | this.index[id] = k; 68 | delete this.deps[REACTIVE][id]; 69 | delete this.deps[REACTIVE_WEAK][id]; 70 | delete this.deps[STATIC][id]; 71 | delete this.deps[STATIC_WEAK][id]; 72 | this.deps[k][id] = true; 73 | } 74 | 75 | static getKind( 76 | type: 'query' | 'subscription' | 'mutation', 77 | directives: ?{}, 78 | ): Kind { 79 | directives = directives || {}; 80 | if ( 81 | (type === 'query' && directives.hasOwnProperty('live')) || 82 | (type === 'subscription' && !directives.hasOwnProperty('static')) 83 | ) { 84 | return directives.hasOwnProperty('weak') ? REACTIVE_WEAK : REACTIVE; 85 | } else { 86 | return directives.hasOwnProperty('weak') ? STATIC_WEAK : STATIC; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/db/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import regeneratorRuntime from 'regenerator-runtime'; // for async/await work flow 4 | 5 | import type { DocumentNode } from 'graphql'; 6 | import graphql from 'graphql-anywhere'; 7 | import hash from 'object-hash'; 8 | 9 | import { lww, set, ron2js } from '@swarm/rdt'; 10 | import Op, { Frame } from '@swarm/ron'; 11 | import UUID, { ZERO } from '@swarm/ron-uuid'; 12 | import API from '@swarm/api'; 13 | import type { Options, Value } from '@swarm/api'; 14 | import type { Atom } from '@swarm/ron'; 15 | 16 | import type { Request, Response } from './types'; 17 | import { GQLSub } from './subscription'; 18 | 19 | export { default as UUID } from '@swarm/ron-uuid'; 20 | export { Verbose } from '@swarm/client/lib/connection'; 21 | export { LocalStorage, InMemory } from '@swarm/client'; 22 | 23 | export type { Request, Response, Variables } from './types'; 24 | export type { Atom } from '@swarm/ron'; 25 | 26 | export default class SwarmDB extends API { 27 | constructor(options: Options): SwarmDB { 28 | super(options); 29 | return this; 30 | } 31 | 32 | async execute( 33 | request: Request, 34 | callback?: (Response) => void, 35 | ): Promise<{ ok: boolean, off?: () => boolean }> { 36 | const h = GQLSub.hash(request, callback); 37 | for (const s of this.subs) { 38 | if (s.is(h)) { 39 | return { ok: false }; 40 | } 41 | } 42 | 43 | if (request.query.definitions.length !== 1) { 44 | throw new Error(`unexpected length of definitions: ${request.query.definitions.length}`); 45 | } 46 | 47 | await this.ensure(); 48 | const sub = new GQLSub(this, this.client, this.cache, request, callback); 49 | this.subs.push(sub); 50 | sub.finalize((h: string) => { 51 | let c = -1; 52 | for (const s of this.subs) { 53 | c++; 54 | if (s.is(h)) { 55 | this.subs.splice(c, 1); 56 | break; 57 | } else { 58 | } 59 | } 60 | }); 61 | const ok = await sub.start(); 62 | return { 63 | ok, 64 | off: () => sub.off(), 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/db/src/schema.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import gql from 'graphql-tag'; 4 | 5 | // Swarm GraphQL schema. 6 | // 7 | // Not in use, just for reference. 8 | export const Schema = gql` 9 | # An UUID instance or string representation. 10 | scalar UUID 11 | 12 | # RON Atom. 13 | # Note that null value is also possible, but cannot be 14 | # defined explicitly in GraphQL. 15 | union Atom = String | Int | Float | Bool | UUID 16 | 17 | # Generic interface to describe a node in the swarm. 18 | # Due to strict nature of types in the GraphQL it's not 19 | # possible to define compound field names, so we have to 20 | # make an agreement that this interface describes all possible 21 | # shapes w/o explicit definition. But we still know that 22 | # at least two field are available. 23 | interface Node { 24 | id: UUID 25 | type: String 26 | version: String 27 | } 28 | 29 | directive @include(if: Bool!) on FIELD 30 | 31 | directive @skip(if: Bool!) on FIELD 32 | 33 | # To be able to define which node must be unwrapped 34 | # Can be missed if the field contains a UUID itself. 35 | # Overrides if id explicitly passed. 36 | # Works also for string representation of UUID if defined w/o 37 | # parameters. 38 | directive @node(id: UUID) on FIELD 39 | 40 | # Casts Set's payload to an array and slice it with given arguments 41 | # A field should either already contains UUID or @node directive 42 | # must be passed first. 43 | directive @slice(offset: Int!, limit: Int) on FIELD 44 | 45 | # Reverse works for Set type only 46 | directive @reverse on FIELD 47 | 48 | # Weak is a directive which adds more control to 49 | # data flow management. This directive tells the runtime 50 | # to call back even if the node is not presented in the resulting 51 | # response. If there is no state for the object in the local storage. 52 | # Useful for offline work. 53 | directive @weak on FIELD 54 | 55 | # Note. Priority of execution of directives from the first to the last. 56 | 57 | schema { 58 | query: Query 59 | mutation: Mutation 60 | subscription: Subscription 61 | } 62 | 63 | # Non-empty POJO with string keys and Atoms as values. 64 | # Just pass 'undefined' as a value to delete a field. 65 | type Payload { 66 | _: Atom 67 | } 68 | 69 | # Operations which can be applied to certain nodes. 70 | # Different operations for different types, depending 71 | # on their CRDTs. 72 | # 73 | # Note that an error will be raised in case of type mismatch. 74 | type Mutation { 75 | # LWW 76 | set(id: UUID!, payload: Payload!): Bool 77 | 78 | # SET 79 | add(id: UUID!, value: Atom): Bool 80 | remove(id: UUID!, value: Atom): Bool 81 | } 82 | 83 | # An empty object. '_' field used just 84 | # to follow GraphQL syntax. It's possible to describe any 85 | # shape right from the root of subscription, using directives 86 | # above. 87 | type Subscription { 88 | _: Node 89 | } 90 | 91 | # Static directive tells runtime to just fetch a particular node once. 92 | # it's a default behavior for 'query' statement. Hence, useful for 93 | # 'subscription' statement. 94 | directive @static on FIELD 95 | 96 | type Query { 97 | _: Node 98 | } 99 | 100 | # Live directive tells runtime to subscribe to a particular node. 101 | # it's a default behavior for 'subscription' statement. Hence, 102 | # useful for 'query' statement. 103 | directive @live on FIELD 104 | `; 105 | -------------------------------------------------------------------------------- /packages/db/src/subscription.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import regeneratorRuntime from 'regenerator-runtime'; // for async/await work flow 4 | 5 | import type { DocumentNode } from 'graphql'; 6 | import graphql from 'graphql-anywhere'; 7 | import hash from 'object-hash'; 8 | 9 | import { lww, set, ron2js } from '@swarm/rdt'; 10 | import Op, { Frame } from '@swarm/ron'; 11 | import UUID, { ZERO } from '@swarm/ron-uuid'; 12 | import API from '@swarm/api'; 13 | import type { Options, Value } from '@swarm/api'; 14 | import type { Atom } from '@swarm/ron'; 15 | 16 | import type { Request, Response, IClient, IApi } from './types'; 17 | import { node, parseDate, applyScalarDirectives } from './utils'; 18 | 19 | import { Dependencies, KINDS, REACTIVE_WEAK } from './deps'; 20 | 21 | export class GQLSub { 22 | cache: { [string]: { [string]: Atom } }; 23 | client: IClient; 24 | api: IApi; 25 | finalizer: ((h: string) => void) | void; 26 | cbk: ((Response) => void) | void; 27 | active: boolean | void; 28 | request: Request; 29 | id: string; // hash from payload object 30 | 31 | deps: Dependencies; 32 | 33 | operation: 'query' | 'mutation' | 'subscription'; 34 | invokeTimer: TimeoutID; 35 | 36 | constructor( 37 | api: IApi, 38 | client: IClient, 39 | cache: { [string]: { [string]: Atom } }, 40 | request: Request, 41 | cbk?: (Response) => void, 42 | ): GQLSub { 43 | this.api = api; 44 | this.request = request; 45 | this.id = GQLSub.hash(request, cbk); 46 | // $FlowFixMe 47 | this.operation = request.query.definitions[0].operation; 48 | 49 | this.client = client; 50 | this.cache = cache; 51 | this.cbk = cbk; 52 | this.deps = new Dependencies(); 53 | 54 | // $FlowFixMe 55 | this._invoke = this._invoke.bind(this); 56 | return this; 57 | } 58 | 59 | is(h: string): boolean { 60 | return this.id === h; 61 | } 62 | 63 | off(): boolean { 64 | if (this.active === true) { 65 | let ret = false; 66 | switch (this.operation) { 67 | case 'query': 68 | case 'subscription': 69 | ret = !!this.client.off('', this._invoke); 70 | this.active = !ret; 71 | break; 72 | case 'mutation': 73 | // do nothing actually b/c we have no any real subscriptions 74 | this.active = ret = false; 75 | } 76 | this.finalizer && this.finalizer(this.id); 77 | return ret; 78 | } 79 | return false; 80 | } 81 | 82 | finalize(f: (h: string) => void): void { 83 | this.finalizer = f; 84 | } 85 | 86 | async start(): Promise { 87 | if (this.active !== undefined) return false; 88 | switch (this.operation) { 89 | case 'query': 90 | case 'subscription': 91 | this.active = true; 92 | this.callback(); 93 | break; 94 | case 'mutation': 95 | this.active = true; 96 | const res = await this.runMutation(); 97 | this.off(); 98 | return res; 99 | default: 100 | throw new Error(`unknown operation: '${this.operation}'`); 101 | } 102 | return this.active || false; 103 | } 104 | 105 | _invoke(l: string, s: string | null): void { 106 | // prevent unauthorized calls 107 | if (this.active === false) { 108 | this.client.off('', this._invoke); 109 | return; 110 | } 111 | clearTimeout(this.invokeTimer); 112 | 113 | // passable values: 114 | // - null 115 | // - {version: '0', id: , type: ''} // server told that there is no data 116 | // - full state 117 | let v = null; 118 | if (s !== null) v = ron2js(s || l); 119 | // console.log('_invoke', { v, l, s }); 120 | 121 | let id; 122 | const head = Op.fromString(l); 123 | if (head && !head.object.eq(ZERO)) { 124 | id = head.object.toString(); 125 | } else return; 126 | 127 | // $FlowFixMe ? 128 | this.cache[id] = v; 129 | this.invokeTimer = setTimeout(() => this.callback(), 0); 130 | } 131 | 132 | subscribe(): void { 133 | for (const kind of KINDS) { 134 | const on = this.deps.toString(kind); 135 | if (on) { 136 | const options = this.deps.options(kind); 137 | this.client.on(on, this._invoke, options); 138 | } 139 | } 140 | } 141 | 142 | callback(): void { 143 | if (this.active !== true) return; 144 | const { ready, tree, deps } = this.buildTree(); 145 | 146 | const diff = this.deps.diff(deps); 147 | this.deps = deps; 148 | const off = diff.toString(); 149 | if (off) this.client.off(off, this._invoke); 150 | this.subscribe(); 151 | 152 | if (!ready) return; 153 | 154 | const { cbk } = this; 155 | if (cbk) { 156 | if (this.operation !== 'mutation') { 157 | // drop this sub from 158 | if (Object.keys(deps.index).length === 0) { 159 | this.off(); 160 | } 161 | cbk({ 162 | data: tree, 163 | off: () => this.off(), 164 | }); 165 | } else { 166 | cbk({ data: tree }); 167 | } 168 | } 169 | } 170 | 171 | buildTree(): { 172 | tree: Value, 173 | deps: Dependencies, 174 | ready: boolean, 175 | } { 176 | const ctx = { 177 | ready: true, 178 | deps: new Dependencies(), 179 | }; 180 | 181 | const tree = graphql( 182 | this.resolver.bind(this), 183 | this.request.query, 184 | {}, 185 | ctx, 186 | this.request.variables, 187 | ); 188 | 189 | return { 190 | tree, 191 | ready: ctx.ready, 192 | deps: ctx.deps, 193 | }; 194 | } 195 | 196 | resolver( 197 | fieldName: string, 198 | root: { [string]: Atom }, 199 | args: { [string]: Atom }, 200 | context: { ready: boolean, deps: Dependencies }, 201 | info: { 202 | isLeaf: boolean, 203 | directives: { [string]: { [string]: Atom } } | void, 204 | }, 205 | ): mixed { 206 | if (root instanceof UUID) return null; 207 | 208 | // workaround type 209 | if (fieldName === 'type') fieldName = 'type'; 210 | 211 | let value: Atom = root[fieldName]; 212 | if (typeof value === 'undefined') value = null; 213 | 214 | // get UUID from @node directive if presented 215 | // thus, override the value if `id` argument passed 216 | value = node(value, info.isLeaf, info.directives); 217 | 218 | // if atom value is not a UUID or is a leaf, return w/o 219 | // any additional business logic 220 | if (!(value instanceof UUID) || info.isLeaf) { 221 | return applyScalarDirectives(value, info.directives); 222 | } 223 | 224 | const kind = Dependencies.getKind(this.operation, info.directives); 225 | const ensure = (kind | 1) !== kind; 226 | const reactive = (kind | 2) !== kind; 227 | const id = value.toString(); 228 | // $FlowFixMe 229 | let obj: Value = this.cache[id]; 230 | 231 | if (reactive || typeof obj === 'undefined' || (!obj && ensure)) { 232 | context.deps.put(kind, id); 233 | } 234 | 235 | context.ready = context.ready && this.cache.hasOwnProperty(id); 236 | 237 | for (const key of Object.keys(info.directives || {})) { 238 | // $FlowFixMe 239 | const dir = info.directives[key]; 240 | switch (key) { 241 | case 'slice': 242 | if (!obj) continue; 243 | if (!Array.isArray(obj)) obj = obj.valueOf(); 244 | // $FlowFixMe 245 | const args = [(dir && dir.begin) || 0]; 246 | if (dir && dir.end) args.push(dir.end); 247 | obj = obj.slice(...args); 248 | break; 249 | case 'reverse': 250 | if (!obj) continue; 251 | if (!Array.isArray(obj)) obj = obj.valueOf(); 252 | obj.reverse(); 253 | break; 254 | } 255 | } 256 | 257 | if (ensure) context.ready = context.ready && !!obj; 258 | 259 | if (!Array.isArray(obj)) return obj; 260 | 261 | for (let i = 0; i < obj.length; i++) { 262 | if (!(obj[i] instanceof UUID)) continue; 263 | // $FlowFixMe 264 | const value = this.cache[obj[i].toString()]; 265 | 266 | if (reactive || typeof value === 'undefined' || (!value && ensure)) { 267 | // $FlowFixMe 268 | context.deps.put(kind, obj[i].toString()); 269 | } 270 | // check if value presented 271 | if (typeof value === 'undefined') { 272 | context.ready = false; 273 | // $FlowFixMe 274 | } else if (ensure) context.ready = context.ready && value && value.id; 275 | // $FlowFixMe 276 | obj[i] = value || null; 277 | } 278 | 279 | return obj; 280 | } 281 | 282 | async runMutation(): Promise { 283 | const ctx = {}; 284 | const tree = graphql( 285 | this.mutation.bind(this), 286 | this.request.query, 287 | {}, 288 | ctx, 289 | this.request.variables, 290 | ); 291 | 292 | const all = []; 293 | 294 | for (const key of Object.keys(tree)) { 295 | const v = tree[key]; 296 | all.push( 297 | Promise.resolve(v).then(ok => { 298 | tree[key] = ok; 299 | }), 300 | ); 301 | } 302 | 303 | Promise.all(all) 304 | .then(() => { 305 | if (this.cbk) this.cbk({ data: tree }); 306 | }) 307 | .catch(error => { 308 | if (this.cbk) this.cbk({ data: null, error }); 309 | }); 310 | 311 | return true; 312 | } 313 | 314 | mutation( 315 | fieldName: string, 316 | root: { [string]: Atom }, 317 | args: { 318 | id: string | UUID, 319 | value?: Atom, 320 | payload?: { [string]: Atom | void }, 321 | }, 322 | context: { [string]: true }, 323 | info: { directives: { [string]: { [string]: Atom } } | void }, 324 | ): mixed { 325 | if (!info.isLeaf) return false; 326 | switch (fieldName) { 327 | case 'set': 328 | if (!args.payload) return false; 329 | return this.api.set(args.id, args.payload); 330 | case 'add': 331 | return this.api.add(args.id, args.value || null); 332 | case 'remove': 333 | return this.api.remove(args.id, args.value || null); 334 | default: 335 | return false; 336 | } 337 | } 338 | 339 | static hash(request: Request, cbk?: (Response<*>) => void): string { 340 | return hash({ request, cbk }); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /packages/db/src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DocumentNode } from 'graphql'; 3 | import type { Atom } from '@swarm/ron'; 4 | import { UUID } from '@swarm/ron'; 5 | 6 | export type Variables = { [string]: Atom | { [string]: Atom } }; 7 | 8 | export type Response = { 9 | data: T, 10 | off?: () => boolean, 11 | error?: Error, 12 | }; 13 | 14 | export type Request = { 15 | query: DocumentNode, 16 | variables?: Variables, 17 | }; 18 | 19 | export interface IClient { 20 | on( 21 | id: string, 22 | cbk: (string, string | null) => void, 23 | options?: { once?: true, ensure?: true }, 24 | ): Promise; 25 | off(id: string, cbk: (string, string | null) => void): string | void; 26 | } 27 | 28 | export interface IApi { 29 | set(id: string | UUID, payload: { [string]: Atom | void }): Promise; 30 | add(id: string | UUID, value: Atom): Promise; 31 | remove(id: string | UUID, value: Atom): Promise; 32 | } 33 | -------------------------------------------------------------------------------- /packages/db/src/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { UUID, Frame } from '@swarm/ron'; 4 | import type { Atom } from '@swarm/ron'; 5 | import { calendarBase2Date } from '@swarm/clock'; 6 | 7 | export function node( 8 | value: Atom, 9 | isLeaf: boolean, 10 | directives: ?{ [string]: { [string]: Atom } } = {}, 11 | ): Atom { 12 | if (directives && directives.hasOwnProperty('node')) { 13 | if (!directives.node && typeof value === 'string') { 14 | return UUID.fromString(value); 15 | } else if (directives.node.id instanceof UUID) { 16 | return directives.node.id; 17 | } else if (typeof directives.node.id === 'string') { 18 | return UUID.fromString(directives.node.id); 19 | } 20 | } else if (!isLeaf && typeof value === 'string') { 21 | return UUID.fromString(value); 22 | } 23 | return value; 24 | } 25 | 26 | export const parseDate = (s: string | UUID): Date => { 27 | const uuid = s instanceof UUID ? s : UUID.fromString(s); 28 | return calendarBase2Date(uuid.value); 29 | }; 30 | 31 | export const applyScalarDirectives = ( 32 | value: Atom, 33 | directives: {} = {}, 34 | ): Atom => { 35 | for (const key of Object.keys(directives || {})) { 36 | switch (key) { 37 | case 'date': 38 | if (typeof value === 'string' || value instanceof UUID) 39 | value = parseDate(value.toString()); 40 | break; 41 | case 'uuid': 42 | if (typeof value === 'string' || value instanceof UUID) 43 | value = UUID.fromString(value.toString()); 44 | break; 45 | } 46 | } 47 | return value; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/rdt/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/rdt/__tests__/empty.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { ron2js } from '../src'; 4 | 5 | test('rdt zero2js', () => { 6 | let set = ron2js('*set#test1!'); 7 | // $FlowFixMe 8 | expect(set.type).toBe('set'); 9 | // $FlowFixMe 10 | expect(set.id).toBe('test1'); 11 | // $FlowFixMe 12 | expect(set.version).toBe('0'); 13 | expect(set).toEqual({}); 14 | 15 | set = ron2js('#test1'); 16 | // $FlowFixMe 17 | expect(set.type).toBe(''); 18 | // $FlowFixMe 19 | expect(set.id).toBe('test1'); 20 | // $FlowFixMe 21 | expect(set.version).toBe('0'); 22 | expect(set).toEqual({}); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/rdt/__tests__/iheap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import IHeap, {eventComparator, eventComparatorDesc, refComparator} from '../src/iheap'; 4 | import Op, {Batch, ZERO, Frame} from '@swarm/ron'; 5 | 6 | test('IHeap put frame', () => { 7 | const frameA = "*lww#test@time1-orig:number=1@(2:string'2'"; 8 | const frameB = "*lww#test@time3-orig:number=3@(4:string'4'"; 9 | const frameC = "*lww#test@time1-orig:number=1@(2:string'2'@(3:number=3@(4:string'4'"; 10 | 11 | const heap = new IHeap(eventComparator); 12 | 13 | heap.put(new Frame(frameA)); 14 | heap.put(new Frame(frameB)); 15 | 16 | expect(heap.frame().toString()).toBe(frameC); 17 | }); 18 | 19 | test('IHeap op', () => { 20 | const frames = new Batch( 21 | new Frame("*lww#test@time1-orig:number=1@(2:string'2'"), 22 | new Frame("*lww#test@time3-orig:number=3@(4:string'4'"), 23 | new Frame("*lww#test@time2-orig:number=2@(2:string'2'@(3:number=3@(4:string'4'"), 24 | ); 25 | 26 | const heap = new IHeap(refComparator); 27 | heap.put(frames); 28 | // $FlowFixMe 29 | const loc = heap.current().uuid(3); 30 | let count = 0; 31 | 32 | while ( 33 | // $FlowFixMe 34 | heap 35 | .current() 36 | .uuid(3) 37 | .eq(loc) 38 | ) { 39 | count++; 40 | heap.next(); 41 | } 42 | expect(count).toBe(3); 43 | }); 44 | 45 | test('IHeap merge', () => { 46 | const frameA = "*rga#test@1:0'A'@2'B'"; // D E A C B 47 | const frameB = "*rga#test@1:0'A'@3'C'"; 48 | const frameC = "*rga#test@4:0'D'@5'E'"; 49 | const frameR = "*rga#test@4'D'@5'E'@1'A'@3'C'@2'B'"; 50 | const heap = new IHeap(eventComparatorDesc, refComparator); 51 | heap.put(new Frame(frameA)); 52 | heap.put(new Frame(frameB)); 53 | heap.put(new Frame(frameC)); 54 | const res = new Frame(); 55 | while (!heap.eof()) { 56 | const op = heap.current() || ZERO; 57 | res.push(op); 58 | heap.nextPrim(); 59 | } 60 | expect(res.toString()).toBe(frameR); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/rdt/__tests__/log.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {Batch} from '@swarm/ron'; 4 | import {reduce} from '../src'; 5 | 6 | test('log reduce', () => { 7 | const cases = [['*log#id!@2+B:b=2@1+A:a=1', '*log#id@3+C:c=3@1+A:a=1', '*log#id@3+C!:c=3@2+B:b=2@1+A:a=1']]; 8 | 9 | for (const c of cases) { 10 | const result = c.pop(); 11 | expect(reduce(Batch.fromStringArray(...c)).toString()).toBe(result); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /packages/rdt/__tests__/lww.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Batch, UUID } from '@swarm/ron'; 4 | 5 | import { reduce } from '../src'; 6 | import { ron2js } from '../src/lww'; 7 | 8 | import { deepEqual as de } from 'assert'; 9 | 10 | test('lww reduce', () => { 11 | const cases = [ 12 | [ 13 | // 0+o 14 | '*lww#test!', 15 | "*lww#test@time:a'A'", 16 | "*lww#test@time!:a'A'", 17 | ], 18 | [ 19 | // s+o 20 | "*lww#test@1!:a'A'", 21 | "*lww#test@2:b'B'", 22 | "*lww#test@2!@1:a'A'@2:b'B'", 23 | ], 24 | [ 25 | // o+o 26 | "*lww#test@1:a'A1'", 27 | "*lww#test@2:a'A2'", 28 | "*lww#test@2!:a'A2'", 29 | ], 30 | [ 31 | // p+p 32 | "*lww#test@1:d! :a'A1':b'B1':c'C1'", 33 | "*lww#test@2:d! :a'A2':b'B2'", 34 | "*lww#test@2!:a'A2':b'B2'@1:c'C1'", 35 | ], 36 | [ 37 | "*lww#test@0ld!@new:key'new_value'", 38 | "*lww#test@new:key'new_value'", 39 | "*lww#test@new!:key'new_value'", 40 | ], 41 | [ 42 | '#1X8C30K+user!', 43 | "*lww#1X8C30K+user@1X8C30M+user!:some'value'", 44 | "*lww#1X8C30K+user@1X8C30M+user!:some'value'", 45 | ], 46 | [ 47 | '*lww#1_A8H+1_A8Gu71@1_A8Ic8F01+1_A8Gu71!:completed>true', 48 | "*lww#1_A8H+1_A8Gu71@1_A8HE8C02+1_A8Gu71!:completed>false:title'third'", 49 | "*lww#1_A8H+1_A8Gu71@1_A8Ic8F01+1_A8Gu71!:completed>true@(HE8C02+:title'third'", 50 | ], 51 | [ 52 | "*lww#1_AAuOCD01+1_AAuJN~@1_AAvY5201+1_AAvK_p!:completed>false@(uOCz01+(uJN~:title'sixth'", 53 | '*lww#1_AAuOCD01+1_AAuJN~@1_AAvQ2c01+1_AAvJZk!:completed>true', 54 | "*lww#1_AAuOCD01+1_AAuJN~@1_AAvY5201+1_AAvK_p!:completed>false@(uOCz01+(uJN~:title'sixth'", 55 | ], 56 | ]; 57 | 58 | for (const c of cases) { 59 | const result = c.pop(); 60 | expect(reduce(Batch.fromStringArray(...c)).toString()).toBe(result); 61 | } 62 | }); 63 | 64 | test('lww map to js', () => { 65 | const array_ron = "*lww#array@2!@1:~%=0@2:%1'1':%2=1:%3=2:%4>notexists"; 66 | let obj = ron2js(array_ron); 67 | expect(obj).toEqual({ 68 | '0': 0, 69 | '1': '1', 70 | '2': 1, 71 | '3': 2, 72 | '4': UUID.fromString('notexists'), 73 | }); 74 | expect(obj && obj.id).toBe('array'); 75 | expect(obj && obj.type).toBe('lww'); 76 | expect(obj && obj.version).toBe('2'); 77 | expect(obj && obj.length).toBe(5); 78 | expect(Array.prototype.slice.call(obj)).toEqual([ 79 | 0, 80 | '1', 81 | 1, 82 | 2, 83 | UUID.fromString('notexists'), 84 | ]); 85 | 86 | const object_ron = "*lww#obj@2:d!:a'A2':b'B2'@1:c'C1'"; 87 | expect(ron2js(object_ron)).toEqual({ a: 'A2', b: 'B2', c: 'C1' }); 88 | 89 | const array_ref = '*lww#ref@t-o!:~%=1:%1=2:%2>arr'; 90 | expect(ron2js(array_ref)).toEqual({ 91 | '0': 1, 92 | '1': 2, 93 | '2': UUID.fromString('arr'), 94 | }); 95 | 96 | const lww = '*lww#test@time-orig!:key=1:obj>time1-orig'; 97 | expect(ron2js(lww)).toEqual({ key: 1, obj: UUID.fromString('time1-orig') }); 98 | 99 | const array_no = '*lww#ref@t-o!:key>arr:~%=1:~%1=2'; 100 | expect((ron2js(array_no) || { length: 42 }).length).toBeUndefined(); 101 | 102 | const with_refs = ` 103 | #left@2! :key'value' 104 | #right@3! :number=42 105 | *lww#root@1! :one>left :two>right 106 | . 107 | `; 108 | expect(ron2js(with_refs)).toEqual({ 109 | one: UUID.fromString('left'), 110 | two: UUID.fromString('right'), 111 | }); 112 | 113 | expect(ron2js('*lww#1ABC4+user@1ABC7+user!:active>false')).toEqual({ 114 | active: false, 115 | }); 116 | 117 | const t = 118 | "*lww#1ABC1+user@1ABC3+user!:a=42:b'wat':c^0.1:d>false:e>true:f>1ABC2+user"; 119 | expect(ron2js(t)).toEqual({ 120 | a: 42, 121 | b: 'wat', 122 | c: 0.1, 123 | d: false, 124 | e: true, 125 | f: UUID.fromString('1ABC2+user'), 126 | }); 127 | }); 128 | 129 | test('lww override', () => { 130 | expect( 131 | ron2js( 132 | "*lww#10001+demo@10004+demo!:completed>true@(2+:title'123':completed>false", 133 | ), 134 | ).toEqual({ 135 | title: '123', 136 | completed: true, 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/rdt/__tests__/set.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Op, { UUID, Batch, Frame } from '@swarm/ron'; 4 | import { reduce } from '../src'; 5 | import { ron2js } from '../src/set'; 6 | 7 | test('Set reduce', () => { 8 | const fixtures = [ 9 | ['*set#test1@1!@=1', '*set#test1@2:1;', '*set#test1@2!:1,'], 10 | ['*set#test1@3:1;', '*set#test1@4:2;', '*set#test1@4!:2,@3:1,'], 11 | [ 12 | '*set#test1@2!@=2@1=1', 13 | '*set#test1@5!@=5@3:2,@4:1,', 14 | '*set#test1@5!=5@3:2,@4:1,', 15 | ], 16 | [ 17 | '*set#test1@2!@=2@1=1', 18 | '*set#test1@3!@:2,@4:1,', 19 | '*set#test1@5!@=5', 20 | '*set#test1@5!=5@3:2,@4:1,', 21 | ], 22 | [ 23 | '*set#test1@3!@:2,@4:1,', 24 | '*set#test1@5!@=5', 25 | '*set#test1@2!@=2@1=1', 26 | '*set#test1@5!=5@3:2,@4:1,', 27 | ], 28 | ['*set#test1@1=1', '*set#test1@2=2', '*set#test1@2!=2@1=1'], 29 | [ 30 | '*set#test1@1=1', 31 | '*set#test1@2=2', 32 | '*set#test1@3:2,', 33 | '*set#test1@3!:2,@1:0=1', 34 | ], 35 | [ 36 | '*set#object@1ABC1+user!=5', 37 | '*set#object@1ABC2+user!:1ABC1+user,', 38 | '*set#object@1ABC2+user!:1ABC1+user,', 39 | ], 40 | [ 41 | '*set#mice!', 42 | '', 43 | '*set#mice@1YKBoB+1YKBjg?!:1YKBn59j01+1YKBjg,@(mL+:(lC8H01+,@(lA+:(jg4u01+,@(j4+(hM:(hM5g01+(hM,@(hBDz01+(fD:0+>mouse$1YKBfD@[9+:1YKBfD4x01+(fD,@(du8G01+(Up:0+>mouse$1YKBUp@(WS+:1YKBUp6H01+(Up,@(Tw4Q01+(Tw4:0+>mouse$1YKBTw4@[_8r01+[_>mouse$1YKBT_@[Y+(S8U:1YKBS8Ea01+(S8U,@(S0E_01+[0T:0+>mouse$1YKBS0T@(RwB901+(Rw>mouse$1YKBRw@(QPFP01+(QPQ>mouse$1YKBQPQ@[O4B01+[O>mouse$1YKBQO@(OrAJ01+(OrI>mouse$1YKBOrI@[pE401+[p>mouse$1YKBOp@(MS1T01+(MR>mouse$1YKBMR@(EREx01+(ERW>mouse$1YKBERW@[L0v01+[L0K>mouse$1YKBEL0K@[D1f01+[CZ>mouse$1YKBECZ@[BCz01+[B>mouse$1YKBEB@(D93B01+(D9>mouse$1YKBD9@[L4901+(Af:1YKBAg3~01+(Af,@(AJ+(8T:(8T2S01+(8T,@(7R+(4e:(4e9a01+(4e,@(0O+1YKAQ5:1YKAtmEq01+1YKAQ5,@1YKAsB+:(qq2X01+,@(hC+:(e64801+,@(YZ+:(WSE~01+,@(Ut+:(T_Cx01+,@(S4+:(Q5AZ01+,@(OT+(NK:(NK5l01+(NK,@(N2+(LJB:(LJ6J01+(LJB,@(LG2T01+[G:0+>mouse$1YKALG@(KK5501+(KK>mouse$1YKAKK@(Js2m01+(Jr>mouse$1YKAJr@(GbBg01+(GbL>mouse$1YKAGbL@[E0601+1YK7WoK>mouse$1YK7WoK@[1+:1YKAE20S01+1YK7WoK,@1YK8qM+:1YK7fr8w01+,@1YK7as+:(WoCT01+,@(Wa2T01+(Rl:0+>mouse$1YK7Rl@(Tj+:1YK7Rl5C01+(Rl,@(Rj+(QY_:(QZ2701+(QY_,@(QD8g01+[DG:0+>mouse$1YK7QDG@[26R01+[2>mouse$1YK7Q2@(P+(OwY>mouse$1YK7OwY@(Or2J01+[r2>mouse$1YK7Or2@(Nj3x01+(Nj3>mouse$1YK7Nj3@(LZ3D01+(LZ>mouse$1YK7LZ@(Nc8N01+:1YK7KD3M01+(KD,@)2+:(Jh0t01+(JgZ,@)3+:[J4n01+[J,', 44 | '*set#mice@1YKBoB+1YKBjg!:1YKBn59j01+1YKBjg,@(mL+:(lC8H01+,@(lA+:(jg4u01+,@(j4+(hM:(hM5g01+(hM,@(hBDz01+(fD:0+>mouse$1YKBfD@[9+:1YKBfD4x01+(fD,@(du8G01+(Up:0+>mouse$1YKBUp@(WS+:1YKBUp6H01+(Up,@(Tw4Q01+(Tw4:0+>mouse$1YKBTw4@[_8r01+[_>mouse$1YKBT_@[Y+(S8U:1YKBS8Ea01+(S8U,@(S0E_01+[0T:0+>mouse$1YKBS0T@(RwB901+(Rw>mouse$1YKBRw@(QPFP01+(QPQ>mouse$1YKBQPQ@[O4B01+[O>mouse$1YKBQO@(OrAJ01+(OrI>mouse$1YKBOrI@[pE401+[p>mouse$1YKBOp@(MS1T01+(MR>mouse$1YKBMR@(EREx01+(ERW>mouse$1YKBERW@[L0v01+[L0K>mouse$1YKBEL0K@[D1f01+[CZ>mouse$1YKBECZ@[BCz01+[B>mouse$1YKBEB@(D93B01+(D9>mouse$1YKBD9@[L4901+(Af:1YKBAg3~01+(Af,@(AJ+(8T:(8T2S01+(8T,@(7R+(4e:(4e9a01+(4e,@(0O+1YKAQ5:1YKAtmEq01+1YKAQ5,@1YKAsB+:(qq2X01+,@(hC+:(e64801+,@(YZ+:(WSE~01+,@(Ut+:(T_Cx01+,@(S4+:(Q5AZ01+,@(OT+(NK:(NK5l01+(NK,@(N2+(LJB:(LJ6J01+(LJB,@(LG2T01+[G:0+>mouse$1YKALG@(KK5501+(KK>mouse$1YKAKK@(Js2m01+(Jr>mouse$1YKAJr@(GbBg01+(GbL>mouse$1YKAGbL@[E0601+1YK7WoK>mouse$1YK7WoK@[1+:1YKAE20S01+1YK7WoK,@1YK8qM+:1YK7fr8w01+,@1YK7as+:(WoCT01+,@(Wa2T01+(Rl:0+>mouse$1YK7Rl@(Tj+:1YK7Rl5C01+(Rl,@(Rj+(QY_:(QZ2701+(QY_,@(QD8g01+[DG:0+>mouse$1YK7QDG@[26R01+[2>mouse$1YK7Q2@(P+(OwY>mouse$1YK7OwY@(Or2J01+[r2>mouse$1YK7Or2@(Nj3x01+(Nj3>mouse$1YK7Nj3@(LZ3D01+(LZ>mouse$1YK7LZ@(Nc8N01+:1YK7KD3M01+(KD,@)2+:(Jh0t01+(JgZ,@)3+:[J4n01+[J,', 45 | ], 46 | [ 47 | `*set#mice@1YKDY54a01+1YKDY5! 48 | >mouse$1YKDY5`, 49 | 50 | // note ? header 51 | `*set#mice@1YKDXO3201+1YKDXO? 52 | ! 53 | @>mouse$1YKDXO 54 | @(WBF901(WBY>mouse$1YKDWBY 55 | @[67H01[6>mouse$1YKDW6 56 | @(Uh4j01(Uh>mouse$1YKDUh 57 | @(S67V01(S6>mouse$1YKDS6 58 | @(Of(N3:1YKDN3DS01+1YKDN3, 59 | @(MvBV01(IuJ:0>mouse$1YKDIuJ 60 | @(LF:1YKDIuEY01+1YKDIuJ, 61 | :{A601, 62 | @(Io5l01[oA:0>mouse$1YKDIoA 63 | @[l7_01[l>mouse$1YKDIl 64 | @(57(4B:1YKD4B3f01+1YKD4B, 65 | @(0bB401+1YKCsd:0>mouse$1YKCsd 66 | @1YKCu6+:1YKCsd7Q01+1YKCsd,`, 67 | 68 | // `*set#mice@1YKDXO3201+1YKDXO! 69 | // @1YKDY54a01+1YKDY5>mouse$1YKDY5 70 | // @1YKDXO3201+1YKDXO>mouse$1YKDXO 71 | // @(WBF901(WBY>mouse$1YKDWBY 72 | // @[67H01[6>mouse$1YKDW6 73 | // @(Uh4j01(Uh>mouse$1YKDUh 74 | // @(S67V01(S6>mouse$1YKDS6 75 | // @(Of(N3:1YKDN3DS01+1YKDN3, 76 | // @(MvBV01(IuJ:0>mouse$1YKDIuJ 77 | // @(LF:1YKDIuEY01+1YKDIuJ, 78 | // :{A601, 79 | // @(Io5l01[oA:0>mouse$1YKDIoA 80 | // @[l7_01[l>mouse$1YKDIl 81 | // @(57(4B:1YKD4B3f01+1YKD4B, 82 | // @(0bB401+1YKCsd:0>mouse$1YKCsd`, 83 | '*set#mice@1YKDY54a01+1YKDY5!>mouse$1YKDY5@(XO3201+(XO>mouse$1YKDXO@(WBF901+(WBY>mouse$1YKDWBY@[67H01+[6>mouse$1YKDW6@(Uh4j01+(Uh>mouse$1YKDUh@(S67V01+(S6>mouse$1YKDS6@(Of+(N3:1YKDN3DS01+1YKDN3,@(MvBV01+(IuJ:0+>mouse$1YKDIuJ@(LF+:1YKDIuEY01+(IuJ,:{A601+,@(Io5l01+[oA:0+>mouse$1YKDIoA@[l7_01+[l>mouse$1YKDIl@(57+(4B:1YKD4B3f01+(4B,@(0bB401+1YKCsd:0+>mouse$1YKCsd@1YKCu6+:1YKCsd7Q01+1YKCsd,', 84 | ], 85 | ]; 86 | 87 | for (const fixt of fixtures) { 88 | const output = new Frame(fixt.pop()); 89 | const reduced = reduce(Batch.fromStringArray(...fixt)); 90 | expect(reduced.toString()).toBe(output.toString()); 91 | } 92 | }); 93 | 94 | test('Set map to js', () => { 95 | expect(ron2js('*set#test1@2:d!:0=2@1=1').type).toBe('set'); 96 | expect(ron2js('*set#test1@2:d!:0=2@1=1').version).toBe('2'); 97 | expect(ron2js('*set#test1@2:d!:0=2@1=1').length).toBe(2); 98 | expect(ron2js('*set#test1@2:d!:0=2@1=1').id).toBe('test1'); 99 | expect(ron2js('*set#test1@2:d!:0=2@1=1')).toEqual({ '0': 2, '1': 1 }); 100 | 101 | expect(ron2js('*set#test1@3:d!:0>object@2=2@1=1')).toEqual({ 102 | '0': UUID.fromString('object'), 103 | '1': 2, 104 | '2': 1, 105 | }); 106 | expect(ron2js('*set#test1@3:d!:0>object@2=2#test@1=1#test1@=3')).toEqual({ 107 | '0': UUID.fromString('object'), 108 | '1': 2, 109 | '2': 3, 110 | }); 111 | expect(ron2js('*set#object@1ABC3+user!,')).toEqual({}); 112 | expect(ron2js('*set#test1@3:d!:2,@1:0=1')).toEqual({ '0': 1 }); 113 | expect(ron2js('*set#object@1ABC2+user!:1ABC1+user,')).toEqual({}); 114 | }); 115 | 116 | // test('Set bug', () => { 117 | // const frames = [ 118 | // new Frame('*set#mice@1YKCFO0t01+1YKCFN!>mouse$1YKCFN'), 119 | // new Frame( 120 | // '*set#mice@1YKCDR+1YKCC0V?!:1YKCC0EL01+1YKCC0V,@(BrB101+(BrN:0+>mouse$1YKCBrN@[NCI01+[N>mouse$1YKCBN@1YKBwZ+1YKBoc:1YKBuk4E01+1YKBoc,@(qJ+:(od1R01+,@(oB+(jg:(n59j01+(jg,@(mL+:(lC8H01+,@(lA+:(jg4u01+,@(j4+(hM:(hM5g01+(hM,@(hBDz01+(fD:0+>mouse$1YKBfD@[9+:1YKBfD4x01+(fD,@(du8G01+(Up:0+>mouse$1YKBUp@(WS+:1YKBUp6H01+(Up,@(Tw4Q01+(Tw4:0+>mouse$1YKBTw4@[_8r01+[_>mouse$1YKBT_@[Y+(S8U:1YKBS8Ea01+(S8U,@(S0E_01+[0T:0+>mouse$1YKBS0T@(RwB901+(Rw>mouse$1YKBRw@(QPFP01+(QPQ>mouse$1YKBQPQ@[O4B01+[O>mouse$1YKBQO@(OrAJ01+(OrI>mouse$1YKBOrI@[pE401+[p>mouse$1YKBOp@(MS1T01+(MR>mouse$1YKBMR@(EREx01+(ERW>mouse$1YKBERW@[L0v01+[L0K>mouse$1YKBEL0K@[D1f01+[CZ>mouse$1YKBECZ@[BCz01+[B>mouse$1YKBEB@(D93B01+(D9>mouse$1YKBD9@[L4901+(Af:1YKBAg3~01+(Af,@(AJ+(8T:(8T2S01+(8T,@(7R+(4e:(4e9a01+(4e,@(0O+1YKAQ5:1YKAtmEq01+1YKAQ5,@1YKAsB+:(qq2X01+,@(hC+:(e64801+,@(YZ+:(WSE~01+,@(Ut+:(T_Cx01+,@(S4+:(Q5AZ01+,@(OT+(NK:(NK5l01+(NK,@(N2+(LJB:(LJ6J01+(LJB,@(LG2T01+[G:0+>mouse$1YKALG@(KK5501+(KK>mouse$1YKAKK@(Js2m01+(Jr>mouse$1YKAJr@(GbBg01+(GbL>mouse$1YKAGbL@[E0601+1YK7WoK>mouse$1YK7WoK@[1+:1YKAE20S01+1YK7WoK,@1YK8qM+:1YK7fr8w01+,@1YK7as+:(WoCT01+,@(Wa2T01+(Rl:0+>mouse$1YK7Rl@(Tj+:1YK7Rl5C01+(Rl,@(Rj+(QY_:(QZ2701+(QY_,@(QD8g01+[DG:0+>mouse$1YK7QDG@[26R01+[2>mouse$1YK7Q2@(P+(OwY>mouse$1YK7OwY@(Or2J01+[r2>mouse$1YK7Or2@(Nj3x01+(Nj3>mouse$1YK7Nj3@(LZ3D01+(LZ>mouse$1YK7LZ@(Nc8N01+:1YK7KD3M01+(KD,@)2+:(Jh0t01+(JgZ,@)3+:[J4n01+[J,', 121 | // ), 122 | // new Frame('*set#mice@1YKCDR+1YKCC0V!@(FO0t01+(FN>mouse$1YKCFN@(DR+(C0V,'), 123 | // ]; 124 | 125 | // let c = 1; 126 | // for (const f of frames) { 127 | // console.log('frame', c++); 128 | // for (const op of f) { 129 | // console.log(op.toString()); 130 | // } 131 | // } 132 | // }); 133 | -------------------------------------------------------------------------------- /packages/rdt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/rdt", 3 | "version": "0.1.1", 4 | "description": "Swarm Replicated Data Types - Basic", 5 | "author": "Victor Grishchenko ", 6 | "contributors": [ 7 | "Oleg Lebedev (https://github.com/olebedev)" 8 | ], 9 | "main": "lib/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "dependencies": { 15 | "@swarm/ron": "^0.1.1", 16 | "@swarm/ron-uuid": "^0.1.1" 17 | }, 18 | "files": [ 19 | "lib/*.js", 20 | "lib/*.js.flow" 21 | ], 22 | "scripts": { 23 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 24 | "build:clean": "../../node_modules/.bin/rimraf lib", 25 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 26 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 27 | }, 28 | "keywords": [ 29 | "swarm", 30 | "replicated", 31 | "RON", 32 | "CRDT" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /packages/rdt/src/iheap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Op, { UUID, Frame, Batch, Cursor, ZERO } from '@swarm/ron'; 4 | 5 | export default class IHeap { 6 | iters: Array; 7 | primary: (Op, Op) => number; 8 | secondary: ?(Op, Op) => number; 9 | 10 | constructor(primary: (Op, Op) => number, secondary?: (Op, Op) => number) { 11 | this.iters = new Array(1); 12 | this.primary = primary; 13 | this.secondary = secondary; 14 | } 15 | 16 | _less(i: number, j: number): boolean { 17 | const ii = this.iters[i].op || ZERO; 18 | const jj = this.iters[j].op || ZERO; 19 | let c = this.primary(ii, jj); 20 | if (c === 0 && this.secondary) { 21 | c = this.secondary(ii, jj); 22 | } 23 | return c < 0; 24 | } 25 | 26 | _sink(i: number) { 27 | let to = i; 28 | let j = i << 1; 29 | if (j < this.iters.length && this._less(j, i)) { 30 | to = j; 31 | } 32 | j++; 33 | 34 | if (j < this.iters.length && this._less(j, to)) { 35 | to = j; 36 | } 37 | 38 | if (to !== i) { 39 | this._swap(i, to); 40 | this._sink(to); 41 | } 42 | } 43 | 44 | _raise(i: number) { 45 | const j = i >> 1; 46 | if (j > 0 && this._less(i, j)) { 47 | this._swap(i, j); 48 | if (j > 1) { 49 | this._raise(j); 50 | } 51 | } 52 | } 53 | 54 | _swap(i: number, j: number) { 55 | const memo = this.iters[i]; 56 | this.iters[i] = this.iters[j]; 57 | this.iters[j] = memo; 58 | } 59 | 60 | get length(): number { 61 | return this.iters.length; 62 | } 63 | 64 | put(input: Frame | Batch) { 65 | const batch = input instanceof Batch ? input : new Batch(input); 66 | for (const item of batch) { 67 | const cursor = new Cursor(item.body); 68 | while (cursor.op && !cursor.op.isRegular()) cursor.next(); 69 | if (cursor.op && cursor.op.isRegular()) { 70 | const at = this.iters.length; 71 | this.iters.push(cursor); 72 | this._raise(at); 73 | } 74 | } 75 | } 76 | 77 | current(): ?Op { 78 | return this.iters.length > 1 ? this.iters[1].op : null; 79 | } 80 | 81 | _remove(i: number) { 82 | if (this.iters.length === 2 && i === 1) { 83 | this.clear(); 84 | } else { 85 | if (this.iters.length - 1 === i) { 86 | this.iters.pop(); 87 | } else { 88 | this.iters.splice(i, 1, this.iters.pop()); 89 | } 90 | this._sink(i); 91 | } 92 | } 93 | 94 | _next(i: number) { 95 | this.iters[i].next(); 96 | if (!this.iters[i].op || this.iters[i].op.isHeader()) { 97 | this._remove(i); 98 | } else { 99 | this._sink(i); 100 | } 101 | } 102 | 103 | next(): ?Op { 104 | this._next(1); 105 | return this.current(); 106 | } 107 | 108 | eof(): boolean { 109 | return this.iters.length <= 1; 110 | } 111 | 112 | clear() { 113 | this.iters = new Array(1); 114 | } 115 | 116 | frame(): Frame { 117 | const cur = new Frame(); 118 | while (!this.eof()) { 119 | const op = this.current(); 120 | if (op) { 121 | cur.push(op); 122 | } 123 | this.next(); 124 | } 125 | return cur; 126 | } 127 | 128 | nextPrim(): ?Op { 129 | const eqs: Array = []; 130 | this._listEqs(1, eqs); 131 | if (eqs.length > 1) { 132 | eqs.sort(); 133 | } 134 | for (let i = eqs.length - 1; i >= 0; i--) { 135 | this._next(eqs[i]); 136 | // this._sink(eqs[i]); 137 | } 138 | return this.current(); 139 | } 140 | 141 | _listEqs(at: number, eqs: Array) { 142 | eqs.push(at); 143 | const l = at << 1; 144 | if (l < this.iters.length) { 145 | if ( 146 | 0 === this.primary(this.iters[1].op || ZERO, this.iters[l].op || ZERO) 147 | ) { 148 | this._listEqs(l, eqs); 149 | } 150 | const r = l | 1; 151 | if (r < this.iters.length) { 152 | if ( 153 | 0 === this.primary(this.iters[1].op || ZERO, this.iters[r].op || ZERO) 154 | ) { 155 | this._listEqs(r, eqs); 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | function comparator( 163 | n: 0 | 1 | 2 | 3, 164 | desc: boolean = false, 165 | ): (Op, Op) => number { 166 | return (...args: Array): number => { 167 | if (desc) args.reverse(); 168 | return args[0].uuid(n).compare(args[1].uuid(n)); 169 | }; 170 | } 171 | 172 | export const eventComparator = comparator(2); 173 | export const eventComparatorDesc = comparator(2, true); 174 | export const refComparator = comparator(3); 175 | export const refComparatorDesc = comparator(3, true); 176 | -------------------------------------------------------------------------------- /packages/rdt/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | 4 | import Op, { Batch, Frame, FRAME_SEP } from '@swarm/ron'; 5 | import type { Atom } from '@swarm/ron'; 6 | import UUID, { ZERO } from '@swarm/ron-uuid'; 7 | 8 | import lww from './lww'; 9 | import log from './log'; 10 | import set from './set'; 11 | 12 | const rdt: { [string]: { type: UUID, reduce: Batch => Frame } } = { 13 | lww, 14 | log, 15 | set, 16 | }; 17 | 18 | function empty(batch: Batch): Frame { 19 | const ret = new Frame(); 20 | for (const first of batch.frames) { 21 | for (const op of first) { 22 | let loc = op.uuid(3); 23 | if (!op.isHeader()) loc = op.uuid(2); 24 | ret.push( 25 | new Op( 26 | op.uuid(0), 27 | op.uuid(1), 28 | // $FlowFixMe 29 | batch.frames[batch.length - 1][Symbol.iterator]().op.event, 30 | loc, 31 | undefined, 32 | FRAME_SEP, 33 | ), 34 | ); 35 | return ret; 36 | } 37 | } 38 | return ret; 39 | } 40 | 41 | // Reduce picks a reducer function, performs all the sanity checks, 42 | // invokes the reducer, returns the result 43 | export function reduce(batch: Batch): Frame { 44 | let type = ZERO; 45 | for (const first of batch.frames) { 46 | for (const op of first) { 47 | type = op.type; 48 | break; 49 | } 50 | if (!type.eq(ZERO)) break; 51 | } 52 | 53 | if (rdt[type.toString()]) { 54 | return rdt[type.toString()].reduce(batch); 55 | } 56 | return empty(batch); 57 | } 58 | 59 | export function ron2js(rawFrame: string): { [string]: Atom } | null { 60 | for (const op of new Frame(rawFrame)) { 61 | switch (true) { 62 | case lww.type.eq(op.type): 63 | return lww.ron2js(rawFrame); 64 | case set.type.eq(op.type): 65 | return set.ron2js(rawFrame); 66 | case ZERO.eq(op.type): 67 | const v = set.ron2js(rawFrame); 68 | // $FlowFixMe 69 | Object.getPrototypeOf(v).type = ''; 70 | return v; 71 | } 72 | } 73 | return null; 74 | } 75 | 76 | export { default as lww } from './lww'; 77 | export { default as log } from './log'; 78 | export { default as set } from './set'; 79 | -------------------------------------------------------------------------------- /packages/rdt/src/log.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | 4 | import Op, {Batch, Frame, FRAME_SEP} from '@swarm/ron'; 5 | import UUID, {ZERO} from '@swarm/ron-uuid'; 6 | import IHeap, {eventComparatorDesc} from './iheap'; 7 | 8 | const heap = new IHeap(eventComparatorDesc); 9 | 10 | export function reduce(batch: Batch): Frame { 11 | const ret = new Frame(); 12 | if (!batch.length) return ret; 13 | 14 | for (const frame of batch) { 15 | if (batch.length === 1) return frame; 16 | for (const op of frame) { 17 | const head = new Op(type, op.uuid(1), op.uuid(2), ZERO, undefined, FRAME_SEP); 18 | 19 | const theLastOne = Op.fromString(batch.frames[batch.length - 1].toString()); 20 | if (theLastOne) head.event = theLastOne.event; 21 | 22 | ret.push(head); 23 | 24 | heap.clear(); 25 | heap.put(batch); 26 | 27 | while (!heap.eof()) { 28 | const current = heap.current(); 29 | if (!current || current.event.sep !== '+') break; 30 | ret.pushWithTerm(current, ','); 31 | heap.nextPrim(); 32 | } 33 | return ret; 34 | } 35 | } 36 | return ret; 37 | } 38 | 39 | export const type = UUID.fromString('log'); 40 | export default {reduce, type}; 41 | -------------------------------------------------------------------------------- /packages/rdt/src/lww.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Op, { Frame, Batch, FRAME_SEP } from '@swarm/ron'; 4 | import type { Atom } from '@swarm/ron'; 5 | import UUID, { ZERO } from '@swarm/ron-uuid'; 6 | import IHeap, { refComparator, eventComparatorDesc } from './iheap'; 7 | 8 | export const type = UUID.fromString('lww'); 9 | const heap = new IHeap(refComparator, eventComparatorDesc); 10 | 11 | // Last-write-wins reducer. 12 | export function reduce(batch: Batch): Frame { 13 | batch = batch.filter(f => !!f.body); 14 | const ret = new Frame(); 15 | if (!batch.length) return ret; 16 | batch.sort().reverse(); 17 | 18 | for (const frame of batch) { 19 | if (batch.length === 1) return frame; 20 | for (const op of frame) { 21 | ret.push(new Op(type, op.uuid(1), op.uuid(2), ZERO, undefined, FRAME_SEP)); 22 | 23 | heap.clear(); 24 | heap.put(batch); 25 | 26 | while (!heap.eof()) { 27 | const current = heap.current(); 28 | if (!current) break; 29 | ret.pushWithTerm(current, ','); 30 | heap.nextPrim(); 31 | } 32 | 33 | return ret; 34 | } 35 | } 36 | 37 | return ret; 38 | } 39 | 40 | export function ron2js(rawFrame: string): { [string]: Atom } { 41 | const ret = {}; 42 | const proto = {}; 43 | proto.type = 'lww'; 44 | const lww = new Frame(rawFrame); 45 | let length: number = 0; 46 | let latest = ZERO; 47 | 48 | for (const op of lww.unzip().reverse()) { 49 | if (!proto.id) { 50 | proto.id = op.uuid(1).toString(); 51 | proto.uuid = op.uuid(1); 52 | latest = op.event; 53 | } 54 | if (!op.uuid(1).eq(proto.uuid) || !op.isRegular()) continue; 55 | 56 | let value = op.value(0); 57 | 58 | let key = op.location.toString(); 59 | if (op.location.isHash()) { 60 | if (op.location.value !== '~') { 61 | throw new Error('only flatten arrays are being supported'); 62 | } 63 | key = op.location.origin; 64 | } 65 | if (length > -1) { 66 | const p = parseInt(key); 67 | if (!isNaN(p)) { 68 | length = Math.max(p + 1, length); 69 | } else { 70 | length = -1; 71 | } 72 | } 73 | ret[key] = { 74 | value: value, 75 | writable: false, 76 | enumerable: true, 77 | }; 78 | } 79 | 80 | proto.version = latest.toString(); 81 | 82 | if (Object.keys(ret).length > 1 && length > 0) { 83 | proto.length = length; 84 | proto.values = function() { 85 | return Array.prototype.slice.call(this); 86 | }; 87 | // $FlowFixMe 88 | proto.valueOf = function() { 89 | return this.values(); 90 | }; 91 | proto[Symbol.iterator] = function() { 92 | return this.values()[Symbol.iterator](); 93 | }; 94 | proto.toJSON = function() { 95 | return JSON.stringify( 96 | this.values().map(i => { 97 | if (i instanceof UUID) { 98 | return '#' + i.toString(); 99 | } else return i; 100 | }), 101 | ); 102 | }; 103 | } 104 | 105 | return Object.create(proto, ret); 106 | } 107 | 108 | export default { reduce, type, ron2js }; 109 | -------------------------------------------------------------------------------- /packages/rdt/src/set.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Op, { ZERO as ZERO_OP, FRAME_SEP, Batch, Frame, Cursor } from '@swarm/ron'; 4 | import type { Atom } from '@swarm/ron'; 5 | import UUID, { ZERO } from '@swarm/ron-uuid'; 6 | import IHeap, { refComparatorDesc } from './iheap'; 7 | 8 | export const type = UUID.fromString('set'); 9 | const heap = new IHeap(setComparator, refComparatorDesc); 10 | 11 | // Set, fully commutative, with tombstones. 12 | // You can either add or remove an atom/tuple. 13 | // Equal elements possible. 14 | export function reduce(batch: Batch): Frame { 15 | batch = batch.filter(f => !!f.body); 16 | const ret = new Frame(); 17 | if (!batch.length) return ret; 18 | batch.sort().reverse(); 19 | 20 | for (const frame of batch) { 21 | if (batch.length === 1) return frame; 22 | for (const op of frame) { 23 | ret.push(new Op(type, op.uuid(1), op.uuid(2), ZERO, undefined, FRAME_SEP)); 24 | 25 | heap.clear(); 26 | heap.put(batch); 27 | 28 | while (!heap.eof()) { 29 | const current = heap.current(); 30 | if (!current) break; 31 | ret.pushWithTerm(current, ','); 32 | heap.nextPrim(); 33 | } 34 | return ret; 35 | } 36 | } 37 | 38 | return ret; 39 | } 40 | 41 | export function setComparator(a: Op, b: Op): number { 42 | let ae = a.uuid(2); 43 | let be = b.uuid(2); 44 | if (!a.uuid(3).isZero()) ae = a.uuid(3); 45 | if (!b.uuid(3).isZero()) be = b.uuid(3); 46 | return -ae.compare(be); 47 | } 48 | 49 | export function ron2js(rawFrame: string): { [string]: Atom } { 50 | const set = new Frame(rawFrame); 51 | const values: { [string]: true } = {}; 52 | const ret = {}; 53 | let latest = ZERO; 54 | const proto = { 55 | length: 0, 56 | id: '', 57 | uuid: ZERO, 58 | type: 'set', 59 | version: '', 60 | values: function() { 61 | return Array.prototype.slice.call(this); 62 | }, 63 | valueOf: function() { 64 | return this.values(); 65 | }, 66 | [Symbol.iterator]: function() { 67 | return this.values()[Symbol.iterator](); 68 | }, 69 | toJSON: function() { 70 | return JSON.stringify( 71 | this.values().map(i => { 72 | if (i instanceof UUID) { 73 | return '#' + i.toString(); 74 | } else return i; 75 | }), 76 | ); 77 | }, 78 | }; 79 | 80 | for (const op of set) { 81 | if (op.event.gt(latest)) latest = op.event; 82 | if (!proto.id) { 83 | proto.id = op.uuid(1).toString(); 84 | proto.uuid = Object.freeze(op.uuid(1)); 85 | } 86 | if (!op.uuid(1).eq(proto.uuid) || !op.isRegular()) continue; 87 | if (op.values && !values[op.values]) { 88 | values[op.values] = true; 89 | ret[proto.length++] = { 90 | value: op.value(0), 91 | writable: false, 92 | enumerable: true, 93 | }; 94 | } 95 | } 96 | proto.version = latest.toString(); 97 | return Object.create(proto, ret); 98 | } 99 | 100 | export default { reduce, type, setComparator, ron2js }; 101 | -------------------------------------------------------------------------------- /packages/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "loose": true, 5 | "useBuiltIns": false 6 | }], 7 | "react" 8 | ], 9 | "plugins": [ 10 | ["transform-object-rest-spread", { 11 | "useBuiltIns": true } 12 | ], 13 | "transform-es2015-spread", 14 | "transform-flow-strip-types" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # Swarm + React 2 | 3 | > bindings between Swarm and React 4 | 5 | ### Usage 6 | 7 | Take a look at [test case](https://github.com/gritzko/swarm/blob/master/packages/react/__tests__/react.js) 8 | -------------------------------------------------------------------------------- /packages/react/__tests__/__snapshots__/graphql.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`React: graphql 1`] = ` 4 |
5 | {"object":{"id":"object","version":"1ABC1+user","test":5,"some":null,"additional":null}} 6 | 11 |
12 | `; 13 | 14 | exports[`React: graphql 2`] = ` 15 |
16 | {"object":{"id":"object","version":"1ABC1+user","test":5,"some":"value","additional":null}} 17 | 22 |
23 | `; 24 | 25 | exports[`React: graphql 3`] = ` 26 |
27 | {"object":{"id":"object","version":"1ABC3+user","test":6,"some":"value","additional":null}} 28 | 33 |
34 | `; 35 | 36 | exports[`React: graphql 4`] = ` 37 |
38 | {"object":{"id":"object","version":"1ABC3+user","test":6,"some":"value","additional":true}} 39 | 44 |
45 | `; 46 | -------------------------------------------------------------------------------- /packages/react/__tests__/graphql.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | import gql from 'graphql-tag'; 6 | 7 | import DB from '@swarm/db'; 8 | import { Provider, GraphQL } from '../src'; 9 | import type { Response } from '../src'; 10 | import { Connection } from '../../__tests__/fixtures'; 11 | import { InMemory } from '../../client/src/storage'; 12 | 13 | const Basic = ({ data, error, onClick }) => ( 14 |
15 | {JSON.stringify(data || error)} 16 | 17 |
18 | ); 19 | 20 | const sub = gql` 21 | subscription test { 22 | object @node(id: "object") { 23 | id 24 | version 25 | test 26 | some 27 | additional 28 | } 29 | } 30 | `; 31 | 32 | const mutateObj = gql` 33 | mutation obj($payload: Payload!) { 34 | set(id: "object", payload: $payload) 35 | } 36 | `; 37 | 38 | test('React: graphql', async () => { 39 | const upstream = new Connection('018-react-graphql.ron'); 40 | const storage = new InMemory(); 41 | const api = new DB({ 42 | storage, 43 | upstream, 44 | db: { 45 | id: 'user', 46 | name: 'test', 47 | auth: 'JwT.t0k.en', 48 | clockMode: 'Logical', 49 | }, 50 | }); 51 | 52 | const component = renderer.create( 53 | 54 | 55 | {props => { 56 | return ( 57 | ) => { 60 | // $FlowFixMe 61 | const p = payload || { test: props.data.object.test + 1 }; 62 | if (props.mutations) { 63 | // $FlowFixMe 64 | props.mutations.obj({ payload: p }); 65 | } 66 | }} 67 | /> 68 | ); 69 | }} 70 | 71 | , 72 | ); 73 | 74 | await new Promise(r => setTimeout(r, 500)); 75 | let tree = component.toJSON(); 76 | expect(tree).toMatchSnapshot(); 77 | 78 | component.root.findByType('button').props.onClick({ some: 'value' }); 79 | 80 | await new Promise(r => setTimeout(r, 100)); 81 | 82 | tree = component.toJSON(); 83 | expect(tree).toMatchSnapshot(); 84 | 85 | component.root.findByType('button').props.onClick(); 86 | 87 | await new Promise(r => setTimeout(r, 100)); 88 | 89 | tree = component.toJSON(); 90 | expect(tree).toMatchSnapshot(); 91 | 92 | component.root.findByType('button').props.onClick({ additional: true }); 93 | 94 | await new Promise(r => setTimeout(r, 100)); 95 | 96 | tree = component.toJSON(); 97 | expect(tree).toMatchSnapshot(); 98 | 99 | const dump = upstream.dump(); 100 | expect(dump.session).toEqual(dump.fixtures); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/react", 3 | "version": "0.1.2", 4 | "description": "Swarm to React bindings", 5 | "author": "Oleg Lebedev (https://github.com/olebedev)", 6 | "main": "lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "" 10 | }, 11 | "dependencies": { 12 | "@swarm/api": "^0.1.1", 13 | "@swarm/db": "^0.1.2", 14 | "@swarm/ron": "^0.1.1", 15 | "@swarm/ron-uuid": "^0.1.1" 16 | }, 17 | "devDependencies": { 18 | "babel-preset-react": "^6.24.1", 19 | "react": "16.2.0", 20 | "react-dom": "16.2.0", 21 | "react-test-renderer": "^16.2.0" 22 | }, 23 | "peerDependencies": { 24 | "invariant": "^2.2.2", 25 | "prop-types": "^15.6.0", 26 | "react": "16.2.0", 27 | "react-dom": "16.2.0", 28 | "regenerator-runtime": "^0.11.1" 29 | }, 30 | "files": [ 31 | "lib/*.js", 32 | "lib/*.js.flow" 33 | ], 34 | "scripts": { 35 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 36 | "build:clean": "../../node_modules/.bin/rimraf lib", 37 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 38 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 39 | }, 40 | "keywords": [ 41 | "swarm", 42 | "react", 43 | "replicated", 44 | "RON", 45 | "CRDT" 46 | ], 47 | "publishConfig": { 48 | "access": "public" 49 | }, 50 | "license": "MIT" 51 | } 52 | -------------------------------------------------------------------------------- /packages/react/src/GraphQL.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import regeneratorRuntime from 'regenerator-runtime'; // for async/await work flow 4 | 5 | import * as React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import invariant from 'invariant'; 8 | import type { DocumentNode } from 'graphql'; 9 | 10 | import DB from '@swarm/db'; 11 | import type { Request, Response as DBResponse } from '@swarm/db'; 12 | import type { Value } from '@swarm/api'; 13 | import type { Variables } from '@swarm/db'; 14 | import type { Atom } from '@swarm/ron'; 15 | import UUID, { ERROR } from '@swarm/ron-uuid'; 16 | 17 | export type Mutation = Variables => Promise; 18 | 19 | export type Response = { 20 | data: ?T, 21 | uuid: ?() => UUID, 22 | error?: Error, 23 | mutations: { [string]: Mutation }, 24 | }; 25 | 26 | type Props = { 27 | query?: DocumentNode, 28 | variables?: Variables, 29 | swarm?: DB, 30 | mutations?: { [string]: DocumentNode }, 31 | children: (r: Response) => React.Node, 32 | }; 33 | 34 | type State = { 35 | data: ?T, 36 | error?: Error, 37 | mutations: { 38 | [string]: Mutation, 39 | }, 40 | initialized: boolean, 41 | }; 42 | 43 | export default class GraphQL extends React.Component, State> { 44 | swarm: ?DB; 45 | _off: void | (() => boolean); 46 | _unmounted: boolean; 47 | 48 | constructor(props: Props, context: { swarm: ?DB }) { 49 | super(props, context); 50 | this.swarm = context.swarm || this.props.swarm; 51 | invariant( 52 | this.swarm, 53 | `Could not find "swarm" in either the context or ` + 54 | `props of . ` + 55 | `Either wrap the root component in a , ` + 56 | `or explicitly pass "swarm" as a prop to .`, 57 | ); 58 | 59 | this._unmounted = false; 60 | this.state = { 61 | data: null, 62 | mutations: this._bindMutations(), 63 | initialized: this.swarm ? this.swarm.client.queue === undefined : false, 64 | }; 65 | 66 | if (this.swarm) { 67 | this.swarm 68 | .ensure() 69 | .then(() => { 70 | this._subscribe(); 71 | this.setState({ initialized: true }); 72 | }) 73 | .catch(error => { 74 | !this._unmounted && this.setState({ error }); 75 | }); 76 | } 77 | } 78 | 79 | componentDidUpdate(prev: Props) { 80 | if ( 81 | this.props.query !== prev.query || 82 | !shallowEqual(this.props.variables, prev.variables) || 83 | !shallowEqual(this.props.mutations, prev.mutations) 84 | ) { 85 | this._unsubscribe(); 86 | !this._unmounted && 87 | this.setState( 88 | { 89 | data: null, 90 | error: undefined, 91 | mutations: this._bindMutations(), 92 | }, 93 | this._subscribe.bind(this), 94 | ); 95 | } 96 | } 97 | 98 | componentWillUnmount() { 99 | this._unmounted = true; 100 | this._unsubscribe(); 101 | } 102 | 103 | async _subscribe(): Promise { 104 | const { 105 | props: { query, variables }, 106 | swarm, 107 | } = this; 108 | if (!swarm || !swarm.execute || !query) return; 109 | 110 | const sub = await swarm.execute({ query, variables }, (r: DBResponse) => { 111 | !this._unmounted && this.setState({ data: r.data, error: r.error }); 112 | }); 113 | 114 | if (this._off) this._off(); 115 | this._off = sub.off; 116 | } 117 | 118 | _unsubscribe() { 119 | this._off && this._off(); 120 | } 121 | 122 | _bindMutations(): { 123 | [string]: Mutation, 124 | } { 125 | const { 126 | props: { mutations }, 127 | swarm, 128 | } = this; 129 | const ret = {}; 130 | if (mutations && swarm) { 131 | for (const key of Object.keys(mutations)) { 132 | ret[key] = async (variables: Variables): Promise => { 133 | return new Promise((resolve, reject) => { 134 | swarm.execute({ query: mutations[key], variables }, (r: DBResponse) => { 135 | r.error ? reject(r.error) : resolve(r.data); 136 | }); 137 | }); 138 | }; 139 | } 140 | } 141 | return ret; 142 | } 143 | 144 | render() { 145 | return ( 146 | this.props.children && 147 | this.props.children.call(null, { 148 | data: this.state.data, 149 | uuid: this.state.initialized && this.swarm ? this.swarm.uuid : null, 150 | error: this.state.error, 151 | mutations: this.state.mutations, 152 | }) 153 | ); 154 | } 155 | } 156 | 157 | GraphQL.contextTypes = { 158 | swarm: PropTypes.shape({}).isRequired, 159 | }; 160 | 161 | function shallowEqual(a?: {}, b?: {}): boolean { 162 | if (a === b) return true; 163 | if (a == null || b == null) return false; 164 | var aKeys = Object.keys(a); 165 | if (Object.keys(b).length !== aKeys.length) return false; 166 | for (const key of aKeys) { 167 | if (a[key] !== b[key]) return false; 168 | } 169 | return true; 170 | } 171 | -------------------------------------------------------------------------------- /packages/react/src/Provider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import API from '@swarm/db'; 6 | 7 | export default class Provider extends React.Component<{ 8 | swarm: API, 9 | children: React.ChildrenArray, 10 | }> { 11 | getChildContext() { 12 | return { swarm: this.props.swarm }; 13 | } 14 | 15 | render() { 16 | let { children } = this.props; 17 | return React.Children.only(children); 18 | } 19 | } 20 | 21 | Provider.childContextTypes = { 22 | swarm: PropTypes.shape({}).isRequired, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/react/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export { default as Provider } from './Provider'; 4 | export { default as GraphQL } from './GraphQL'; 5 | export type { Response, Mutation } from './GraphQL'; 6 | -------------------------------------------------------------------------------- /packages/regular-grammar/README.md: -------------------------------------------------------------------------------- 1 | # Regular language parser generator 2 | 3 | Gram is a macro substitution utility for regular expressions. 4 | Create huge regexes with ease: 5 | 6 | ```js 7 | const BABY_ENGLISH = { 8 | 9 | WORD: /\w+/, 10 | ADJECTIVE: /good|bad/, 11 | SENTENCE: /($ADJECTIVE)\s+($WORD)/ 12 | 13 | }; 14 | 15 | const sent = resolve("SENTENCE", BABY_ENGLISH); 16 | // produces: /(good|bad)\s+(\w+)/ 17 | 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /packages/regular-grammar/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function resolve(rule_name, rules) { 2 | var rule = rules[rule_name]; 3 | var pattern = rule.source.replace(/\$(\w+)/g, function(match, name) { 4 | var parser = resolve(name, rules); 5 | var pattern = parser.source 6 | .replace(/\((?!\?:)/g, '(?:') 7 | .replace(/(\\\\)*\\\(\?:/g, '$1\\('); 8 | return pattern; 9 | }); 10 | 11 | return pattern === rule.pattern 12 | ? rule 13 | : (rules[rule_name] = new RegExp(pattern)); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/regular-grammar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regular-grammar", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "A simple regular grammar/parser generator", 6 | "author": "Victor Grishchenko ", 7 | "main": "index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "" 11 | }, 12 | "files": [ 13 | "test.js", 14 | "index.js", 15 | "README.md" 16 | ], 17 | "scripts": { 18 | "test": "node ./test.js" 19 | }, 20 | "keywords": [ 21 | "regular", 22 | "grammar", 23 | "protocol", 24 | "parser", 25 | "regex" 26 | ], 27 | "license": "MIT" 28 | } 29 | -------------------------------------------------------------------------------- /packages/regular-grammar/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const resolve = require('./index'); 3 | 4 | const BABY_ENGLISH = { 5 | WORD: /\w+/, 6 | ADJECTIVE: /good|bad/, 7 | SENTENCE: /($ADJECTIVE)\s+($WORD)/, 8 | }; 9 | 10 | const sent = resolve('SENTENCE', BABY_ENGLISH); 11 | 12 | sent.test('good dog') || process.exit(1); 13 | 14 | const RON_GRAMMAR = { 15 | BASE64: /[0-9A-Za-z_~]/, 16 | INT: /([\([{}\])])?($BASE64{0,10})/, 17 | UUID: /([`\\|\/])?($INT)([-+$%])?($INT)/, 18 | 19 | INT_ATOM: /[+-]?\d{1,17}/, 20 | STRING_ATOM: /"(\\"|[^"])*"/, 21 | FLOAT_ATOM: /[+-]?\d{0,19}\.\d{1,19}([Ee][+-]?\d{1,3})?/, 22 | UUID_ATOM: /(?:($UUID),?)+/, 23 | FRAME_ATOM: /!/, 24 | QUERY_ATOM: /\?/, 25 | 26 | ATOM: /=($INT_ATOM)|($STRING_ATOM)|\^($FLOAT_ATOM)|>($UUID_ATOM)|($FRAME_ATOM)|($QUERY_ATOM)/, 27 | OP: /\s*\.?($UUID)\s*#?($UUID)\s*@?($UUID)\s*:?($UUID)\s*((?:$ATOM){1,8})/, 28 | FRAME: /($OP)+/, 29 | }; 30 | 31 | resolve('FRAME', RON_GRAMMAR); 32 | 33 | const frame = '#id`=1#id`=1@}^0.1'; 34 | RON_GRAMMAR.FRAME.exec(frame)[0] === frame || process.exit(2); 35 | const not_frame = '#id`,``=1@2^0.2'; 36 | RON_GRAMMAR.FRAME.exec(not_frame)[0] !== not_frame || process.exit(3); 37 | 38 | test('regular-grammar', () => { 39 | expect('~').toBe('~'); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/ron-grammar/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/ron-grammar/README.md: -------------------------------------------------------------------------------- 1 | # Regex parsers for the Replicated Object Notation 2 | 3 | see protocol docs at 4 | https://github.com/gritzko/ron 5 | 6 | use: 7 | ``` 8 | > const RON=require('swarm-ron-grammar'); 9 | > RON.OP.exec('#time-orig`:loc=1"str"') 10 | [ '#id`:l=1"str"', 11 | '', 12 | 'time-orig', 13 | '`', 14 | 'loc', 15 | '=1"str"', 16 | index: 0, 17 | input: '#id`:l=1"str"' ] 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/ron-grammar/__tests__/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import RON from '../src'; 3 | import {equal as eq, ok} from 'assert'; 4 | 5 | function tester(re) { 6 | return new RegExp('^(?:' + re.source + ')$', ''); 7 | } 8 | 9 | const UUID = tester(RON.UUID); 10 | ok(UUID.test('0')); 11 | ok(UUID.test('$name')); 12 | ok(UUID.test('time-origin')); 13 | ok(UUID.test('-origin')); 14 | ok(UUID.test('')); 15 | ok(!UUID.test('--')); 16 | 17 | const ATOM = tester(RON.ATOM); 18 | ok(ATOM.test('=1')); 19 | ok(!ATOM.test('=1=')); 20 | ok(!ATOM.test("'''")); 21 | ok(ATOM.test("'\\''")); 22 | ok(ATOM.test("'\\\\\\''")); 23 | ok(!ATOM.test("'\\\\\\'")); 24 | ok(ATOM.test("'\\u000a'")); 25 | 26 | ok(ATOM.test("'\"single-quoted \\'\"'")); 27 | ok(ATOM.test('\'{"json":"not terrible"}\'')); 28 | 29 | ok(ATOM.test('^3.1415')); 30 | ok(ATOM.test('^.1')); 31 | ok(ATOM.test('^1.0e6')); 32 | ok(!ATOM.test('^1e6')); // mandatory . 33 | ok(!ATOM.test('^1')); 34 | ok(ATOM.test('^0.0')); 35 | 36 | ok(ATOM.test('>true')); 37 | ok(ATOM.test('>false')); 38 | 39 | const FRAME = tester(RON.FRAME); 40 | ok(FRAME.test('*lww#`@`:`>end')); 41 | ok(FRAME.test('#$name?')); 42 | ok(FRAME.test('*lww#time-orig@`:key=1')); 43 | ok(FRAME.test("*lww#name@time-orig!:key=1:string'str'")); 44 | 45 | ok(FRAME.test('*lww#test@time-orig:ref>>another.')); 46 | ok(FRAME.test("*lww#test@time-orig!:A=1,:B'2':C>3.")); 47 | 48 | test('~', () => { 49 | expect('~').toBe('~'); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/ron-grammar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/ron-grammar", 3 | "version": "0.1.1", 4 | "description": "A regular grammar for the Swarm Replicated Object Notation", 5 | "author": "Victor Grishchenko ", 6 | "contributors": [ 7 | "Oleg Lebedev (https://github.com/olebedev)" 8 | ], 9 | "main": "lib/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "files": [ 15 | "lib/*.js", 16 | "lib/*.js.flow" 17 | ], 18 | "scripts": { 19 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 20 | "build:clean": "../../node_modules/.bin/rimraf lib", 21 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 22 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 23 | }, 24 | "keywords": [ 25 | "swarm", 26 | "replicated", 27 | "ron", 28 | "grammar", 29 | "protocol", 30 | "parser", 31 | "regex" 32 | ], 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /packages/ron-grammar/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const RON_GRAMMAR: { [string]: RegExp } = { 4 | BASE64: /[0-9A-Za-z_~]/, 5 | UNICODE: /\\u[0-9a-fA-F]{4}/, 6 | INT: /([\([{}\])])?($BASE64{0,10})/, 7 | UUID: /($INT)?([-+$%])?($INT)?/, 8 | 9 | INT_ATOM: /[+-]?\d{1,17}/, 10 | UUID_ATOM: /[`]?$UUID/, 11 | STRING_ATOM: /($UNICODE|\\[^\n\r]|[^'\\\n\r])*/, 12 | FLOAT_ATOM: /[+-]?\d{0,19}\.\d{1,19}([Ee][+-]?\d{1,3})?/, 13 | OPTERM: /[!?,;]/, 14 | FRAMETERM: /\s*[.]/, 15 | 16 | ATOM: /=($INT_ATOM)|'($STRING_ATOM)'|\^($FLOAT_ATOM)|>($UUID)/, 17 | OP: /(?:\s*\*\s*($UUID_ATOM))?(?:\s*#\s*($UUID_ATOM))?(?:\s*@\s*($UUID_ATOM))?(?:\s*:\s*($UUID_ATOM))?\s*((?:\s*$ATOM)*)\s*($OPTERM)?/, 18 | FRAME: /($OP)+$FRAMETERM?/, 19 | }; 20 | 21 | resolve('FRAME', RON_GRAMMAR); 22 | 23 | function resolve(rule_name: string, rules: { [string]: RegExp }): RegExp { 24 | var rule = rules[rule_name]; 25 | var pattern = rule.source.replace(/\$(\w+)/g, function(match, name) { 26 | var parser = resolve(name, rules); 27 | var pattern = parser.source.replace(/\((?!\?:)/g, '(?:').replace(/(\\\\)*\\\(\?:/g, '$1\\('); 28 | return pattern; 29 | }); 30 | 31 | return pattern === rule.pattern ? rule : (rules[rule_name] = new RegExp(pattern)); 32 | } 33 | 34 | export default RON_GRAMMAR; 35 | -------------------------------------------------------------------------------- /packages/ron-uuid/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/ron-uuid/README.md: -------------------------------------------------------------------------------- 1 | ## UIDs 2 | 3 | (from https://github.com/gritzko/swarm-ron-docs/tree/2.0) 4 | 5 | Swarm RON UIDs are roughly equivalent to [RFC4122 Version 1 UUIDs][uuid]. 6 | These are 128-bit logical timestamps serving as globally unique identifiers to *anything*. 7 | RON UIDs employ different formats mostly to achieve better compression. 8 | 9 | Regular RON UIDs have two components: *time* (calendar time) and *origin* (process/replica id). 10 | Both parts are [64-bit ints](int.md). 11 | Transcendent UIDs have origin of 0. 12 | Those are global constants precisely defined mathematically, hence independent of any origin or time. 13 | 14 | As UIDs are extensively used in the protocol, the format is made as compact as possible, while preserving human readability. 15 | RON UIDs are serialized in [Base64](int.md) instead of decimal or hex. 16 | Also, the full 128-bit bit capacity is often excessive, so unnecessary tail bits are zeroed and trimmed. 17 | 18 | UIDs are serialized to Base64 as two [Base64x64 ints](int.md) using `-` as a pair separator, e.g. `1CQKneD1-X~` (time `1CQKneD1`, origin replica id `X~`). 19 | 20 | ### Time part format 21 | 22 | Swarm timestamps are Gregorian calendar based, not milliseconds-since-epoch because they are [hybrid][hybrid] (logical, but calendar-aware). 23 | Intuitive understanding of timestamps is a higher priority than easy calculation of time intervals. 24 | 25 | Timestamp values have the `MMDHmSssnn` format. 26 | Namely, ten 6-bit values encode months-since-epoch, days, hours, minutes, seconds, milliseconds and an additional sequence number. 27 | The resulting bit loss is tolerable (no month is 64 days long). 28 | In case of Base64 serialization, those ten positions correspond to ten Base64 chars. 29 | With binary serializations, those are lower 60 bits of a 64-bit integer. 30 | 31 | The resulting resolution is ~4mln timestamps per second, which is often excessive. 32 | It is OK to shorten timestamps by zeroing the tail (sequence number, milliseconds, etc). 33 | For example, `1CQAneD` is 7 chars and `1CQAn` is 5 chars (`MMDHm`, no seconds - Fri May 27 20:50:00 UTC 2016) 34 | 35 | Time value of `~` means "infinity"/"never". 36 | Time value of `~~~~~~~~~~` means "error". 37 | 38 | ### Origin part format 39 | 40 | In general, RON accepts any 60-bit globally unique replica identifiers. 41 | It is OK to use MAC addressses or random numbers. 42 | 43 | Still, it is strongly recommended to use hierarchical [replica ids](replica.md) of four parts: peer id, server id, user id and session id bits. 44 | For example, in the [0163 scheme](replica.md), replica id `Xgritzk0_D` has server id `X`, user id `gritzk` and session id `0_D`. 45 | 46 | 47 | Theoretically, Swarm RON UIDs are based on a product of two very basic models: Lamport timestamps and process trees. 48 | It is like sequential processes (replicas) exchanging messages asynchronously AND those processes can fork off child replicas. 49 | 50 | ### Transcendent UID format 51 | 52 | Transcendent values use arbitrary 60-bit values. 53 | Typically, those are short human-readable strings in [Base64x64](int.md), e.g. `inc`, `sum`, `txt` and so on. 54 | A bit counter-intuitively, all such constants (like reducer UIDs) are *very* big numbers. 55 | For example, `inc` [decodes](int.md) to 824893205576155136. 56 | 57 | 58 | [lamport]: https://en.wikipedia.org/wiki/Lamport_timestamps 59 | [hybrid]: https://www.cse.buffalo.edu/tech-reports/2014-04.pdf 60 | [mslamp]: http://research.microsoft.com/en-us/um/people/lamport/pubs/time-clocks.pdf 61 | [uuid]: https://tools.ietf.org/html/rfc4122#section-4.2 62 | -------------------------------------------------------------------------------- /packages/ron-uuid/__tests__/format.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import UUID from '../src'; 4 | 5 | test('UUID compression', () => { 6 | const cases = [ 7 | ['}DcR-L8w', '}IYI-', '}IYI-0'], 8 | ['0', '1', '1'], 9 | ['0', '123-0', '123-'], 10 | ['0', '0000000001-orig', ')1-orig'], 11 | ['1time01-src', '1time02+src', '{2+'], 12 | ['hash%here', 'hash%there', '%there'], 13 | ['0', 'name$0', 'name'], 14 | ['time-orig', 'time1+orig2', '(1+(2'], 15 | ['0$author', 'name$author2', 'name${2'], 16 | ['1', ')1', '0000000001-'], 17 | ['time+orig', 'time1+orig2', '(1+(2'], 18 | ['}DcR-}L8w', '}IYI-', '}IYI-0'], 19 | ['A$B', 'A-B', '-'], 20 | ['[1s9L3-[Wj8oO', '[1s9L3-(2Biejq', '-(2Biejq'], 21 | ]; 22 | 23 | for (const c of cases) { 24 | const ctx = UUID.fromString(c[0]); 25 | expect(ctx).toBeDefined(); 26 | expect(ctx).not.toBe(null); 27 | const uuid = UUID.fromString(c[1]); 28 | expect(uuid).toBeDefined(); 29 | expect(uuid).not.toBe(null); 30 | expect(uuid.toString(ctx)).toBe(c[2]); 31 | } 32 | }); 33 | 34 | test('UUID parse compressed', () => { 35 | const cases = [ 36 | ['0', '1', '1'], // 0 37 | ['1-x', ')1', '1000000001-x'], 38 | ['test-1', '-', 'test-1'], 39 | ['hello-111', '[world', 'helloworld-111'], 40 | ['helloworld-111', '[', 'hello-111'], 41 | ['100001-orig', '[', '1-orig'], // 5 42 | ['1+orig', '(2-', '10002-orig'], 43 | ['time+orig', '(1(2', 'time1+orig2'], 44 | // TODO ['name$user', '$scoped', 'scoped$user'], 45 | ['any-thing', 'hash%here', 'hash%here'], 46 | ['0123456789-abcdefghij', ')~)~', '012345678~-abcdefghi~'], 47 | ['0123G-abcdb', '(4566(efF', '01234566-abcdefF'], 48 | ['[1s9L3-[Wj8oO', '-(2Biejq', '[1s9L3-(2Biejq'], // 9 49 | ['(2-[1jHH~', '-[00yAl', '(2-}yAl'], 50 | ]; 51 | 52 | for (const c of cases) { 53 | const ctx = UUID.fromString(c[0]); 54 | expect(ctx).toBeDefined(); 55 | expect(ctx).not.toBe(null); 56 | const uuid = UUID.fromString(c[1], ctx); 57 | expect(uuid).toBeDefined(); 58 | expect(uuid).not.toBe(null); 59 | expect(uuid.toString()).toBe(c[2]); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /packages/ron-uuid/__tests__/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import UUID, {ZERO, ERROR, Vector, Iter} from '../src'; 3 | import {equal as eq, ok} from 'assert'; 4 | 5 | const simple = UUID.fromString('time-orig'); 6 | eq(simple.value, 'time'); 7 | eq(simple.origin, 'orig'); 8 | eq(simple.sep, '-'); 9 | const simple1 = UUID.fromString('time1-orig'); 10 | ok(simple1.ge(simple)); 11 | ok(!simple1.eq(simple)); 12 | eq(simple1.toString(), 'time1-orig'); 13 | eq(simple1.toString(simple), '(1'); 14 | eq(UUID.fromString('newtime-orig').toString(simple), 'newtime-'); 15 | ok(simple.isTime()); 16 | ok(simple.isEvent()); 17 | 18 | const lww = UUID.fromString('lww'); 19 | eq(lww.sep, '$'); 20 | eq(lww.value, 'lww'); 21 | eq(lww.origin, '0'); 22 | ok(!lww.eq(UUID.fromString('lww-0'))); 23 | eq(lww.toString(), 'lww'); 24 | ok(lww.isName()); 25 | ok(!lww.isTime()); 26 | 27 | const hash = UUID.fromString('1234567890%abcdefghij'); 28 | ok(hash.isHash()); 29 | 30 | const zero = UUID.fromString('0'); 31 | eq(zero.value, '0'); 32 | eq(zero.origin, '0'); 33 | eq(zero.sep, '-'); 34 | eq(zero.toString(ERROR), '0'); 35 | eq(zero.toString(zero), '0'); 36 | 37 | const varname = UUID.fromString('$varname'); 38 | eq(varname.toString(), '$varname'); 39 | eq(varname.origin, 'varname'); 40 | ok(varname.isZero()); 41 | eq(varname.toString(simple), '0$varname'); 42 | 43 | const nice_str = UUID.fromString('1', simple); 44 | eq(nice_str.value, '1'); 45 | eq(nice_str.origin, '0'); 46 | eq(nice_str.sep, '$'); 47 | eq(nice_str.toString(), '1'); 48 | eq(nice_str.toString(simple), '1'); 49 | 50 | eq(ZERO.toString(), '0'); 51 | eq(ZERO.origin, '0'); 52 | 53 | // no prefix compression for meaningful string constants 54 | const str1 = UUID.fromString('long1$'); 55 | const str2 = UUID.fromString('long2$'); 56 | eq(str1.toString(str2), 'long1'); 57 | 58 | const lww1 = UUID.fromString('lww', simple1); 59 | eq(lww1.value, 'lww'); 60 | eq(lww1.sep, '$'); 61 | eq(lww1.origin, '0'); 62 | const lww2 = UUID.fromString('lww', str1); 63 | eq(lww2.value, 'lww'); 64 | eq(lww2.sep, '$'); 65 | eq(lww2.origin, '0'); 66 | 67 | const clone = UUID.fromString('', UUID.fromString('$A')); 68 | eq(clone.toString() + '', '$A'); 69 | 70 | const vec = new Vector(); 71 | const uuids = ['time-origin', 'time01-origin', 'time2-origin2'].map(v => UUID.fromString(v)); 72 | vec.push(uuids[0]); 73 | vec.push(uuids[1]); 74 | vec.push(uuids[2]); 75 | eq(vec.toString(), 'time-origin,[1,(2-{2'); 76 | for (let u of vec) ok(u.eq(uuids.shift())); 77 | eq(uuids.length, 0); 78 | 79 | const zeros = new Iter(',,,'); 80 | ok(zeros.uuid && zeros.uuid.isZero()); 81 | zeros.nextUUID(); 82 | ok(zeros.uuid && zeros.uuid.isZero()); 83 | zeros.nextUUID(); 84 | ok(zeros.uuid && zeros.uuid.isZero()); 85 | zeros.nextUUID(); 86 | ok(zeros.uuid === null); 87 | 88 | var num = UUID.base2int('0000000011'); 89 | eq(num, 65); 90 | 91 | test('~', () => { 92 | expect('~').toBe('~'); 93 | }); 94 | -------------------------------------------------------------------------------- /packages/ron-uuid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/ron-uuid", 3 | "version": "0.1.1", 4 | "description": "A Swarm Replicated Object Notation UUID variant", 5 | "author": "Victor Grishchenko ", 6 | "contributors": [ 7 | "Oleg Lebedev (https://github.com/olebedev)" 8 | ], 9 | "main": "lib/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "dependencies": { 15 | "@swarm/ron-grammar": "^0.1.1" 16 | }, 17 | "files": [ 18 | "lib/*.js", 19 | "lib/*.js.flow" 20 | ], 21 | "scripts": { 22 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 23 | "build:clean": "../../node_modules/.bin/rimraf lib", 24 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 25 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 26 | }, 27 | "keywords": [ 28 | "swarm", 29 | "replicated", 30 | "ron", 31 | "grammar", 32 | "protocol" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /packages/ron-uuid/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | 4 | import RON from '@swarm/ron-grammar'; 5 | 6 | export default class UUID { 7 | value: string; 8 | origin: string; 9 | sep: string; 10 | 11 | constructor(value: string, origin: string, sep: ?string): UUID { 12 | this.value = value; 13 | this.origin = origin; 14 | this.sep = sep || '-'; 15 | return this; 16 | } 17 | 18 | get type(): string { 19 | return this.sep; // TODO swap, phase out 20 | } 21 | 22 | toJSON(): string { 23 | return this.toString(); 24 | } 25 | 26 | toString(ctxUUID?: UUID): string { 27 | const ctx = ctxUUID || ZERO; 28 | if (this.origin === '0') { 29 | // nice shortcuts 30 | if (TIME_CONST[this.value]) { 31 | if (this.sep === '-') return this.value; 32 | } else { 33 | if (this.sep === '$') return this.value; 34 | } 35 | } 36 | if (this.origin === ctx.origin) { 37 | if (this.value === ctx.value) return this.sep === ctx.sep ? '' : this.sep; 38 | let zip = UUID.zip64(this.value, ctx.value); 39 | const expSep = zip === this.value ? '$' : '-'; 40 | return expSep === this.sep ? zip : zip + this.sep; 41 | } else { 42 | const time = UUID.zip64(this.value, ctx.value); 43 | const orig = UUID.zip64(this.origin, ctx.origin); 44 | if (this.sep !== '-' || orig === this.origin) { 45 | return time + this.sep + orig; 46 | } else { 47 | return time ? time + this.sep + orig : this.sep + orig; 48 | } 49 | } 50 | } 51 | 52 | le(uuid: UUID): boolean { 53 | if (uuid.value === this.value) return uuid.origin > this.origin; 54 | return uuid.value > this.value; 55 | } 56 | 57 | ge(uuid: UUID): boolean { 58 | if (uuid.value === this.value) return uuid.origin < this.origin; 59 | return uuid.value < this.value; 60 | } 61 | 62 | gt(uuid: UUID): boolean { 63 | return !this.le(uuid); 64 | } 65 | 66 | lt(uuid: UUID): boolean { 67 | return !this.ge(uuid); 68 | } 69 | 70 | eq(uuid: UUID): boolean { 71 | return this.value === uuid.value && this.origin === uuid.origin && this.sep === uuid.sep; 72 | } 73 | 74 | isZero(): boolean { 75 | return this.value === '0'; 76 | } 77 | 78 | static fromString(string: string, ctxUUID?: ?UUID, offset?: number): UUID { 79 | const ctx = ctxUUID || ZERO; 80 | if (!string) return ctx; 81 | const off = offset === undefined ? 0 : offset; 82 | RE.lastIndex = off; 83 | const m: string[] = RE.exec(string); 84 | if (!m || m.index !== off) return ERROR; 85 | if (offset === undefined && m[0] !== string) return ERROR; 86 | const time = UUID.unzip64(m[1], ctx.value); 87 | if (!m[2] && !m[3] && m[1] === time && !TIME_CONST[time]) { 88 | return new UUID(time, '0', '$'); // nice shortcut 89 | } else if (!m[1] && !m[2] && !m[3]) { 90 | return ctx; 91 | } else { 92 | const orig = UUID.unzip64(m[3], ctx.origin); 93 | return new UUID(time, orig, m[2] || ctx.sep); 94 | } 95 | } 96 | 97 | static unzip64(zip: string, ctx: string): string { 98 | if (!zip) return ctx; 99 | let ret = zip; 100 | const prefix = PREFIXES.indexOf(ret[0]); 101 | if (prefix !== -1) { 102 | let pre = ctx.substr(0, prefix + 4); 103 | while (pre.length < prefix + 4) pre += '0'; 104 | ret = pre + ret.substr(1); 105 | } 106 | while (ret.length > 1 && ret[ret.length - 1] === '0') ret = ret.substr(0, ret.length - 1); 107 | return ret; 108 | } 109 | 110 | static zip64(int: string, ctx: string): string { 111 | if (int === ctx) return ''; 112 | let p = 0; 113 | while (int[p] === ctx[p]) p++; 114 | if (p === ctx.length) while (int[p] === '0') p++; 115 | if (p < 4) return int; 116 | return PREFIXES[p - 4] + int.substr(p); 117 | } 118 | 119 | isTime(): boolean { 120 | return this.sep === '-' || this.sep === '+'; 121 | } 122 | 123 | isEvent(): boolean { 124 | return this.sep === '-'; 125 | } 126 | 127 | isDerived(): boolean { 128 | return this.sep === '+'; 129 | } 130 | 131 | isHash(): boolean { 132 | return this.sep === '%'; 133 | } 134 | 135 | isName(): boolean { 136 | return this.sep === '$'; 137 | } 138 | 139 | // overflows js ints! 140 | static base2int(base: string): number { 141 | var ret = 0; 142 | var i = 0; 143 | while (i < base.length) { 144 | ret <<= 6; 145 | ret |= CODES[base.charCodeAt(i)]; 146 | i++; 147 | } 148 | while (i < 10) { 149 | ret <<= 6; 150 | i++; 151 | } 152 | return ret; 153 | } 154 | 155 | compare(uuid: UUID): number { 156 | if (this.eq(uuid)) return 0; 157 | return this.lt(uuid) ? -1 : 1; 158 | } 159 | 160 | isLocal(): boolean { 161 | return this.origin === LOCAL; 162 | } 163 | 164 | local(): UUID { 165 | return new UUID(this.value, LOCAL, this.sep); 166 | } 167 | 168 | isError(): boolean { 169 | return this.value === '~~~~~~~~~~' || this.origin === '~~~~~~~~~~'; 170 | } 171 | } 172 | 173 | export const ZERO = new UUID('0', '0'); 174 | export const NEVER = new UUID('~', '0'); 175 | export const COMMENT = NEVER; 176 | export const ERROR = new UUID('~~~~~~~~~~', '0'); 177 | export const RE = new RegExp(RON.UUID.source, 'g'); 178 | export const PREFIXES = '([{}])'; 179 | export const TIME_CONST = { '0': 1, '~': 1, '~~~~~~~~~~': 1 }; 180 | export const LOCAL = `~local`; 181 | 182 | export const BASE64 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~'; 183 | export const CODES: Int8Array = new Int8Array(128); 184 | for (let i = 0; i < 128; i++) CODES[i] = -1; 185 | for (let i = 0; i < BASE64.length; i++) CODES[BASE64.charCodeAt(i)] = i; 186 | 187 | export class Vector { 188 | body: string; 189 | defaultUUID: UUID; 190 | last: UUID; 191 | 192 | constructor(uuids: string = '', defaultUUID?: UUID = ZERO): Vector { 193 | this.body = uuids; 194 | this.defaultUUID = defaultUUID || ZERO; 195 | this.last = this.defaultUUID; 196 | return this; 197 | } 198 | 199 | /*:: @@iterator(): Iterator { return ({}: any); } */ 200 | 201 | // $FlowFixMe 202 | [Symbol.iterator](): Iterator { 203 | return new Iter(this.body, this.defaultUUID); 204 | } 205 | 206 | push(newUUID: UUID | string): void { 207 | const uuid = UUID.fromString(newUUID.toString()); 208 | const str = uuid.toString(this.last); 209 | if (this.body) this.body += ','; 210 | this.body += str; 211 | this.last = uuid; 212 | } 213 | 214 | toString(): string { 215 | return this.body; 216 | } 217 | } 218 | 219 | export class Iter { 220 | body: string; 221 | offset: number; 222 | uuid: UUID | null; 223 | 224 | constructor(body: string = '', defaultUUID: UUID = ZERO): Iter { 225 | this.body = body; 226 | this.offset = 0; 227 | this.uuid = defaultUUID; 228 | this.nextUUID(); 229 | return this; 230 | } 231 | 232 | toString(): string { 233 | return this.body.substr(this.offset); 234 | } 235 | 236 | nextUUID(): void { 237 | if (this.offset === this.body.length) { 238 | this.uuid = null; 239 | } else { 240 | this.uuid = UUID.fromString(this.body, this.uuid, this.offset); 241 | if (RE.lastIndex === 0 && this.offset !== 0) this.offset = this.body.length; 242 | else this.offset = RE.lastIndex; 243 | if (this.body[this.offset] === ',') this.offset++; 244 | } 245 | } 246 | 247 | // waiting for https://github.com/prettier/prettier/issues/719 to enable on-save formatting 248 | // 249 | /*:: @@iterator(): Iterator { return ({}: any); } */ 250 | 251 | // $FlowFixMe 252 | [Symbol.iterator](): Iterator { 253 | return this; 254 | } 255 | 256 | next(): IteratorResult { 257 | const ret = this.uuid; 258 | if (ret === null) { 259 | return { done: true }; 260 | } else { 261 | this.nextUUID(); 262 | return { 263 | done: false, 264 | value: ret, 265 | }; 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /packages/ron/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ -------------------------------------------------------------------------------- /packages/ron/TODO: -------------------------------------------------------------------------------- 1 | This parser is ~15 times slower than Ragel/Go. 2 | We don't need a nice API for ops. We need a faster parser for ops. 3 | We need a nice object API on top of ops. 4 | 5 | ## Arch: TypeArray Spec + regex + freeze ## 6 | 7 | [ ] objects[uuid], ron[uuid] 8 | [ ] immutable version graph - merge callback, atom reuse, .freeze() 9 | [ ] backref[uuid] -> uuids 10 | 11 | [ ] alloc-free parser/iterator 12 | [ ] serializer - by the standard API 13 | 14 | 15 | --- 16 | API cleanup 17 | 18 | * Iterator -> Cursor 19 | * Stream recv(), write() functions 20 | * Stream on(), off(), update(), push() expect Cursors (expl typed) 21 | * pure function depot RON.FN 22 | * migrate to a conventional export structure 23 | * omnivorous Cursor constructor 24 | -------------------------------------------------------------------------------- /packages/ron/__tests__/batch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Frame, Batch } from '../src'; 4 | 5 | test('Batch.splitByID', () => { 6 | const str = 7 | '*set#mice@1YKBhBDz01+1YKBfD?*#@!@>mouse$1YKBfD@[9:1YKBfD4x01+1YKBfD,@(du8G01(Up:0>mouse$1YKBUp@(WS:1YKBUp6H01+1YKBUp,@(Tw4Q01(Tw4:0>mouse$1YKBTw4@[_8r01[_>mouse$1YKBT_@[Y(S8U:1YKBS8Ea01+1YKBS8U,@(S0E_01[0T:0>mouse$1YKBS0T@(RwB901(Rw>mouse$1YKBRw@(QPFP01(QPQ>mouse$1YKBQPQ@[O4B01[O>mouse$1YKBQO@(OrAJ01(OrI>mouse$1YKBOrI@[pE401[p>mouse$1YKBOp@(MS1T01(MR>mouse$1YKBMR@(EREx01(ERW>mouse$1YKBERW@[L0v01[L0K>mouse$1YKBEL0K@[D1f01[CZ>mouse$1YKBECZ@[BCz01[B>mouse$1YKBEB@(D93B01(D9>mouse$1YKBD9@[L4901(Af:1YKBAg3~01+1YKBAf,@(AJ(8T:(8T2S01(8T,@(7R(4e:(4e9a01(4e,@(0O+1YKAQ5:1YKAtmEq01+1YKAQ5,@1YKAsB+:(qq2X01,@(hC:(e64801,@(YZ:(WSE~01,@(Ut:(T_Cx01,@(S4:(Q5AZ01,@(OT(NK:(NK5l01(NK,@(N2(LJB:(LJ6J01(LJB,@(LG2T01[G:0>mouse$1YKALG@(KK5501(KK>mouse$1YKAKK@(Js2m01(Jr>mouse$1YKAJr@(GbBg01(GbL>mouse$1YKAGbL@[E0601+1YK7WoK>mouse$1YK7WoK@[1:1YKAE20S01+1YK7WoK,@1YK8qM+:1YK7fr8w01+,@1YK7as+:(WoCT01,@(Wa2T01(Rl:0>mouse$1YK7Rl@(Tj:1YK7Rl5C01+1YK7Rl,@(Rj(QY_:(QZ2701(QY_,@(QD8g01[DG:0>mouse$1YK7QDG@[26R01[2>mouse$1YK7Q2@(P(OwY>mouse$1YK7OwY@(Or2J01[r2>mouse$1YK7Or2@(Nj3x01(Nj3>mouse$1YK7Nj3@(LZ3D01(LZ>mouse$1YK7LZ@(Nc8N01:1YK7KD3M01+1YK7KD,@)2:(Jh0t01(JgZ,@)3:[J4n01[J,'; 8 | const res = Batch.splitByID(str); 9 | expect(res.frames).toHaveLength(1); 10 | 11 | const b = []; 12 | const original = []; 13 | for (const op of res.frames[0]) { 14 | b.push(op.toString()); 15 | } 16 | 17 | for (const op of new Frame(str)) { 18 | original.push(op.toString()); 19 | } 20 | 21 | expect(b).toEqual(original); 22 | 23 | // console.log(JSON.stringify(b, null, 2)); 24 | // console.log(JSON.stringify(original, null, 2)); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/ron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swarm/ron", 3 | "version": "0.1.1", 4 | "description": "Swarm Replicated Object Notation (RON)", 5 | "author": "Victor Grishchenko ", 6 | "contributors": [ 7 | "Oleg Lebedev (https://github.com/olebedev)" 8 | ], 9 | "main": "lib/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "dependencies": { 15 | "@swarm/ron-uuid": "^0.1.1" 16 | }, 17 | "files": [ 18 | "lib/*.js", 19 | "lib/*.js.flow" 20 | ], 21 | "scripts": { 22 | "build": "yarn run build:clean && yarn run build:lib && yarn run build:flow", 23 | "build:clean": "../../node_modules/.bin/rimraf lib", 24 | "build:lib": "../../node_modules/.bin/babel -d lib src --ignore '**/__tests__/**'", 25 | "build:flow": "../../node_modules/.bin/flow-copy-source -v -i '**/__tests__/**' src lib" 26 | }, 27 | "keywords": [ 28 | "swarm", 29 | "replicated", 30 | "ron", 31 | "grammar", 32 | "protocol" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /packages/ron/src/batch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Op, { Frame } from './index'; 4 | import { ZERO } from '@swarm/ron-uuid'; 5 | 6 | export default class Batch implements Iterator { 7 | frames: Array; 8 | index: 0; 9 | 10 | constructor(...frames: Array): Batch { 11 | this.frames = frames; 12 | this.index = 0; 13 | return this; 14 | } 15 | 16 | /*:: @@iterator(): Iterator { return ({}: any); } */ 17 | 18 | // $FlowFixMe - computed property 19 | [Symbol.iterator](): Iterator { 20 | this.index = 0; 21 | return this; 22 | } 23 | 24 | clone(): Batch { 25 | return new Batch(...[...this.frames]); 26 | } 27 | 28 | push(f: Frame): Batch { 29 | this.frames.push(f); 30 | return this; 31 | } 32 | 33 | next(): IteratorResult { 34 | if (this.frames.length > this.index) { 35 | return { 36 | done: false, 37 | value: this.frames[this.index++], 38 | }; 39 | } 40 | return { done: true }; 41 | } 42 | 43 | toString(): string { 44 | let ret: Array = []; 45 | for (const c of this.frames) { 46 | ret.push(c.toString()); 47 | } 48 | return ret.join('\n'); 49 | } 50 | 51 | get length(): number { 52 | return this.frames.length; 53 | } 54 | 55 | get long(): number { 56 | let ret = 0; 57 | for (const c of this.frames) { 58 | ret += c.body.length; 59 | } 60 | return ret; 61 | } 62 | 63 | isEmpty(): boolean { 64 | return !!this.frames.length; 65 | } 66 | 67 | hasFullState(): boolean { 68 | for (const f of this.frames) { 69 | if (f.isFullState()) return true; 70 | break; 71 | } 72 | return false; 73 | } 74 | 75 | equal(other: Batch): boolean { 76 | if (this.long !== other.long) { 77 | return false; 78 | } 79 | 80 | for (let i = 0; i < this.length; i++) { 81 | if (!this.frames[i].equal(other.frames[i])) { 82 | return false; 83 | } 84 | } 85 | 86 | return true; 87 | } 88 | 89 | sort(compare?: (a: Frame, b: Frame) => number): Batch { 90 | this.frames.sort( 91 | compare || 92 | ((a, b) => { 93 | const aop = Op.fromString(a.body); 94 | const bop = Op.fromString(b.body); 95 | if (aop && bop) return aop.uuid(2).compare(bop.uuid(2)); 96 | throw new Error('unable to compare invalid frames'); 97 | }), 98 | ); 99 | return this; 100 | } 101 | 102 | reverse(): Batch { 103 | this.frames.reverse(); 104 | return this; 105 | } 106 | 107 | filter(f: (frame: Frame) => boolean): Batch { 108 | return new Batch(...this.frames.filter(f)); 109 | } 110 | 111 | static fromStringArray(...input: Array): Batch { 112 | return new Batch(...input.map(i => new Frame(i))); 113 | } 114 | 115 | static splitByID(source: string): Batch { 116 | const b = new Batch(); 117 | let id = ZERO; 118 | let c: Frame = new Frame(); 119 | 120 | for (const op of new Frame(source)) { 121 | if (op.uuid(1).eq(id)) { 122 | c.push(op); 123 | } else { 124 | if (!id.eq(ZERO)) { 125 | b.push(c); 126 | } 127 | id = op.uuid(1); 128 | c = new Frame(); 129 | c.push(op); 130 | } 131 | } 132 | b.push(c); 133 | return b; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/ron/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | 4 | import Grammar from '@swarm/ron-grammar'; 5 | import UUID, { ZERO as ZERO_UUID, ERROR, COMMENT } from '@swarm/ron-uuid'; 6 | 7 | export { default as UUID } from '@swarm/ron-uuid'; 8 | export { ERROR as UUID_ERROR } from '@swarm/ron-uuid'; 9 | export { default as Batch } from './batch'; 10 | import Batch from './batch'; 11 | 12 | export type Atom = string | number | boolean | null | UUID; 13 | 14 | const TRUE_UUID = UUID.fromString('true'); 15 | const FALSE_UUID = UUID.fromString('false'); 16 | const NULL_UUID = UUID.fromString('0'); 17 | 18 | // A RON op object. Typically, an Op is hosted in a frame. 19 | // Frames are strings, so Op is sort of a Frame iterator. 20 | export default class Op { 21 | type: UUID; 22 | object: UUID; 23 | event: UUID; 24 | location: UUID; 25 | 26 | values: string; 27 | parsed_values: Array | void; 28 | 29 | term: string; 30 | source: ?string; 31 | 32 | constructor( 33 | type: UUID, 34 | object: UUID, 35 | event: UUID, 36 | location: UUID, 37 | values: ?string, 38 | term: ?string, 39 | ): Op { 40 | this.type = type; 41 | this.object = object; 42 | this.event = event; 43 | this.location = location; 44 | this.values = values || ''; 45 | this.parsed_values = undefined; 46 | 47 | this.term = term || ';'; 48 | this.source = null; // FIXME remove 49 | return this; 50 | } 51 | 52 | value(i: number): Atom { 53 | if (!this.parsed_values) this.parsed_values = ron2js(this.values); 54 | return this.parsed_values[i]; 55 | } 56 | 57 | isHeader(): boolean { 58 | return this.term === '!'; 59 | } 60 | 61 | isQuery(): boolean { 62 | return this.term === '?'; 63 | } 64 | 65 | isRegular(): boolean { 66 | return !this.isHeader() && !this.isQuery(); 67 | } 68 | 69 | isError(): boolean { 70 | return this.event.value === ERROR.value; 71 | } 72 | 73 | isComment(): boolean { 74 | return this.type.eq(COMMENT); 75 | } 76 | 77 | // Get op UUID by index (0-3) 78 | uuid(i: 0 | 1 | 2 | 3): UUID { 79 | switch (i) { 80 | case 0: 81 | return this.type; 82 | case 1: 83 | return this.object; 84 | case 2: 85 | return this.event; 86 | case 3: 87 | return this.location; 88 | default: 89 | throw new Error('incorrect uuid index'); 90 | } 91 | } 92 | 93 | key(): string { 94 | return '*' + this.type.value + '#' + this.object.value; 95 | } 96 | 97 | toString(ctxOp: ?Op): string { 98 | let ret = ''; 99 | const ctx = ctxOp || ZERO; 100 | let expComma = ctx.term !== ';'; 101 | 102 | for (const u of [0, 1, 2, 3]) { 103 | const uuid = this.uuid(u); 104 | const same = ctx.uuid(u); 105 | if (uuid.eq(same)) continue; 106 | let str = uuid.toString(same); 107 | ret += UUID_SEPS[u]; 108 | ret += str; 109 | } 110 | 111 | ret += this.values; 112 | if (!this.values || (expComma && this.term !== ',') || (!expComma && this.term !== ';')) { 113 | ret += this.term; 114 | } 115 | return ret; 116 | } 117 | 118 | clone(): Op { 119 | return new Op(this.type, this.object, this.event, this.location, this.values, this.term); 120 | } 121 | 122 | equal(other: Op): boolean { 123 | return ( 124 | this.uuid(0).eq(other.uuid(0)) && 125 | this.uuid(1).eq(other.uuid(1)) && 126 | this.uuid(2).eq(other.uuid(2)) && 127 | this.uuid(3).eq(other.uuid(3)) && 128 | this.values === other.values && 129 | this.term === other.term 130 | ); 131 | } 132 | 133 | static fromString(body: string, context: ?Op, offset: ?number): ?Op { 134 | let ctx = context || ZERO; 135 | const off = offset || 0; 136 | RE.lastIndex = off; 137 | const m: string[] | void = RE.exec(body); 138 | if (!m || m[0] === '' || m.index !== off) return null; 139 | if (m[1] === COMMENT.value) ctx = ZERO; 140 | let term = m[6]; 141 | if (!term) { 142 | if (ctx.term === '!') { 143 | term = ','; 144 | } else { 145 | term = ctx.term; 146 | } 147 | } 148 | 149 | const ret = new Op( 150 | UUID.fromString(m[1], ctx.type), 151 | UUID.fromString(m[2], ctx.object), 152 | UUID.fromString(m[3], ctx.event), 153 | UUID.fromString(m[4], ctx.location), 154 | m[5], 155 | term, 156 | ); 157 | ret.source = m[0]; 158 | return ret; 159 | } 160 | } 161 | 162 | // Parse RON value atoms. 163 | export function ron2js(values: string): Array { 164 | VALUE_RE.lastIndex = 0; 165 | let m: string[] | void; 166 | const ret = []; 167 | 168 | while ((m = (VALUE_RE.exec(values): string[]))) { 169 | if (m[1]) { 170 | ret.push(parseInt(m[1])); 171 | } else if (m[2]) { 172 | ret.push(JSON.parse('"' + m[2] + '"')); // VALUE_RE returns match w/o single quotes 173 | } else if (m[3]) { 174 | ret.push(parseFloat(m[3])); 175 | } else if (m[4]) { 176 | switch (m[4]) { 177 | case TRUE_UUID.value: 178 | ret.push(true); 179 | break; 180 | case FALSE_UUID.value: 181 | ret.push(false); 182 | break; 183 | case NULL_UUID.value: 184 | ret.push(null); 185 | break; 186 | default: 187 | ret.push(UUID.fromString(m[4])); 188 | } 189 | } 190 | } 191 | return ret; 192 | } 193 | 194 | // Serialize JS primitives into RON atoms. 195 | export function js2ron(values: Array): string { 196 | const ret = values.map(v => { 197 | if (v === undefined) return UUID_ATOM_SEP + ZERO_UUID.toString(); 198 | if (v === null) return UUID_ATOM_SEP + NULL_UUID.toString(); 199 | 200 | switch (v.constructor) { 201 | case String: 202 | const json = JSON.stringify(v); 203 | const escq = json.replace(/'/g, '\\u0027'); 204 | return "'" + escq.substr(1, escq.length - 2) + "'"; 205 | case Number: 206 | return Number.isInteger(v) ? INT_ATOM_SEP + v.toString() : FLOAT_ATOM_SEP + v.toString(); 207 | case UUID: 208 | return UUID_ATOM_SEP + v.toString(); 209 | case Boolean: 210 | return UUID_ATOM_SEP + (v ? TRUE_UUID : FALSE_UUID).toString(); 211 | default: 212 | if (v === Op.FRAME_ATOM) return FRAME_SEP; 213 | if (v === Op.QUERY_ATOM) return QUERY_SEP; 214 | throw new Error('unsupported type'); 215 | } 216 | }); 217 | return ret.join(''); 218 | } 219 | 220 | export const RE = new RegExp(Grammar.OP.source, 'g'); 221 | export const VALUE_RE = new RegExp(Grammar.ATOM.source, 'g'); 222 | export const ZERO = new Op(ZERO_UUID, ZERO_UUID, ZERO_UUID, ZERO_UUID, '>0'); 223 | 224 | export const END = new Op(ERROR, ERROR, ERROR, ERROR, '>~'); 225 | export const PARSE_ERROR = new Op(ERROR, ERROR, ERROR, ERROR, '>parseerror'); 226 | export const REDEF_SEPS = '`'; 227 | export const UUID_SEPS = '*#@:'; 228 | export const FRAME_ATOM = Symbol('FRAME'); 229 | export const QUERY_ATOM = Symbol('QUERY'); 230 | export const INT_ATOM_SEP = '='; 231 | export const FLOAT_ATOM_SEP = '^'; 232 | export const UUID_ATOM_SEP = '>'; 233 | export const FRAME_SEP = '!'; 234 | export const QUERY_SEP = '?'; 235 | 236 | export class Frame { 237 | body: string; 238 | last: Op; 239 | 240 | constructor(str: ?string): Frame { 241 | this.body = str ? str.toString() : ''; 242 | this.last = ZERO; 243 | return this; 244 | } 245 | 246 | // Append a new op to the frame 247 | push(op: Op): void { 248 | if (this.last.isComment()) { 249 | this.last = ZERO; 250 | } 251 | 252 | this.body += op.toString(this.last); 253 | this.last = op; 254 | } 255 | 256 | pushWithTerm(op: Op, term: ',' | '!' | '?' | ';'): void { 257 | if (this.last.isComment()) { 258 | this.last = ZERO; 259 | } 260 | 261 | const clone = op.clone(); 262 | clone.term = term; 263 | 264 | this.body += clone.toString(this.last); 265 | this.last = clone; 266 | } 267 | 268 | /*:: @@iterator(): Iterator { return ({}: any); } */ 269 | 270 | // $FlowFixMe - computed property 271 | [Symbol.iterator](): Iterator { 272 | return new Cursor(this.body); 273 | } 274 | 275 | toString(): string { 276 | return this.body; 277 | } 278 | 279 | mapUUIDs(fn: (UUID, number, number, Op) => UUID) { 280 | this.body = mapUUIDs(this.body, fn); 281 | for (const op of this) { 282 | this.last = op; 283 | } 284 | } 285 | 286 | equal(other: Frame): boolean { 287 | if (this.toString() === other.toString()) return true; 288 | const cursor = new Cursor(other.toString()); 289 | for (const op of this) { 290 | const oop = cursor.op; 291 | if (!oop || !op.equal(oop)) return false; 292 | cursor.next(); 293 | } 294 | return cursor.next().done; 295 | } 296 | 297 | isFullState(): boolean { 298 | for (const op of this) return op.isHeader() && op.uuid(3).isZero(); 299 | return false; 300 | } 301 | 302 | isPayload(): boolean { 303 | for (const op of this) if (op.isRegular()) return true; 304 | return false; 305 | } 306 | 307 | filter(p: Op => boolean): Frame { 308 | const ret = new Frame(); 309 | for (const op of this) if (p(op)) ret.push(op); 310 | return ret; 311 | } 312 | 313 | ID(): UUID { 314 | for (const op of this) return op.uuid(1); 315 | return ZERO_UUID; 316 | } 317 | 318 | unzip(): Op[] { 319 | const cumul: Op[] = []; 320 | for (const op of this) cumul.push(op); 321 | return cumul; 322 | } 323 | } 324 | 325 | // Substitute UUIDs in all of the frame's ops. 326 | // Typically used for macro expansion. 327 | export function mapUUIDs(rawFrame: string, fn: (UUID, number, number, Op) => UUID): string { 328 | const ret = new Frame(); 329 | let index = -1; 330 | for (const op of new Frame(rawFrame)) { 331 | index++; 332 | ret.push( 333 | new Op( 334 | fn(op.type, 0, index, op) || op.type, 335 | fn(op.object, 1, index, op) || op.object, 336 | fn(op.event, 2, index, op) || op.event, 337 | fn(op.location, 3, index, op) || op.location, 338 | op.values, 339 | op.term, 340 | ), 341 | ); 342 | } 343 | return ret.toString(); 344 | } 345 | 346 | // Crop a frame, i.e. make a new [from,till) frame 347 | export function slice(from: Cursor, till: Cursor): string { 348 | if (!from.op) return ''; 349 | if (from.body !== till.body) throw new Error('iterators of different frames'); 350 | let ret = from.op.toString(); 351 | ret += from.body.substring(from.offset + from.length, till.op ? till.offset : undefined); 352 | return ret; 353 | } 354 | 355 | export class Cursor implements Iterator { 356 | body: string; 357 | offset: number; 358 | length: number; 359 | op: ?Op; 360 | ctx: ?Op; 361 | 362 | constructor(body: ?string): Cursor { 363 | this.body = body ? body.toString().trim() : ''; 364 | this.offset = 0; 365 | this.length = 0; 366 | this.op = this.nextOp(); 367 | return this; 368 | } 369 | 370 | toString(): string { 371 | return this.body; 372 | } 373 | 374 | clone(): Cursor { 375 | const ret = new Cursor(this.body); 376 | ret.offset = this.offset; 377 | ret.length = this.length; 378 | ret.op = this.op; 379 | ret.ctx = this.ctx; 380 | return ret; 381 | } 382 | 383 | nextOp(): ?Op { 384 | this.offset += this.length; 385 | if (this.offset === this.body.length) { 386 | this.op = null; 387 | this.length = 1; 388 | } else { 389 | const op = Op.fromString(this.body, this.ctx, this.offset); 390 | this.ctx = op; 391 | if (op) { 392 | if (op.isComment()) this.ctx = ZERO; 393 | if (op.source) this.length = op.source.length; 394 | } 395 | this.op = op; 396 | } 397 | return this.op; 398 | } 399 | 400 | eof(): boolean { 401 | return !this.op; 402 | } 403 | 404 | /*:: @@iterator(): Iterator { return ({}: any); } */ 405 | 406 | // $FlowFixMe - computed property 407 | [Symbol.iterator](): Iterator { 408 | return this; 409 | } 410 | 411 | next(): IteratorResult { 412 | const ret = this.op; 413 | if (ret) { 414 | this.nextOp(); 415 | return { done: false, value: ret }; 416 | } else { 417 | return { done: true }; 418 | } 419 | } 420 | } 421 | --------------------------------------------------------------------------------