{"action":"created","comment":{"url":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/comments/176643253","pull_request_review_id":106383100,"id":176643253,"diff_hunk":"@@ -1,82 +1,86 @@\n-import compensateForLatency from './compensateForLatency';\n-import { Events, RedisPipe } from '../../constants';\n-import publish from './publish';\n import { Meteor } from 'meteor/meteor';\n-import { Random } from 'meteor/random';\n+import { DDPServer } from 'meteor/ddp-server';\n+import { EJSON } from 'meteor/ejson';\n+import { Events, RedisPipe } from '../../constants';\n+import RedisSubscriptionManager from '../../redis/RedisSubscriptionManager';\n+import { getRedisPusher } from \"../../redis/getRedisClient\";\n \n-const dispatchUpdate = function(\n-    optimistic,\n-    collectionName,\n-    channels,\n-    docIds,\n-    fields\n-) {\n-    if (optimistic) {\n-        docIds.forEach(docId => {\n-            compensateForLatency(\n-                channels,\n-                collectionName,\n-                Events.UPDATE,\n-                { _id: docId },\n-                fields\n-            );\n-        });\n+const getWriteFence = function (optimistic) {\n+    if (optimistic && DDPServer._CurrentWriteFence) {\n+        return DDPServer._CurrentWriteFence.get();\n     }\n-\n-    Meteor.defer(() => {\n-        docIds.forEach(docId => {\n-            publish(\n-                collectionName,\n-                channels,\n-                {\n-                    [RedisPipe.EVENT]: Events.UPDATE,\n-                    [RedisPipe.FIELDS]: fields,\n-                    [RedisPipe.DOC]: { _id: docId }\n-                },\n-                docId\n-            );\n-        });\n-    });\n+    return null;\n };\n \n-const dispatchRemove = function(optimistic, collectionName, channels, docIds) {\n-    if (optimistic) {\n-        docIds.forEach(docId => {\n-            compensateForLatency(channels, collectionName, Events.REMOVE, {\n-                _id: docId\n+const dispatchEvents = function (fence, events, channels) {\n+    if (fence) {\n+        const write = fence.beginWrite();\n+        RedisSubscriptionManager.queue.queueTask(() => {\n+            events.forEach(({ event, dedicatedChannel }) => {\n+                channels.forEach(channelName => {\n+                    RedisSubscriptionManager.process(channelName, event);\n+                });\n+                RedisSubscriptionManager.process(dedicatedChannel, event);\n             });\n+            write.committed();\n         });\n     }\n \n     Meteor.defer(() => {\n-        docIds.forEach(docId => {\n-            publish(\n-                collectionName,\n-                channels,\n-                {\n-                    [RedisPipe.EVENT]: Events.REMOVE,\n-                    [RedisPipe.DOC]: { _id: docId }\n-                },\n-                docId\n-            );\n+        const client = getRedisPusher();\n+        events.forEach(({ event, dedicatedChannel }) => {\n+            const message = EJSON.stringify(event);\n+            channels.forEach(channelName => {\n+                client.publish(channelName, message);\n+            });\n+            client.publish(dedicatedChannel, message);\n         });\n     });\n };\n \n-const dispatchInsert = function(optimistic, collectionName, channels, docId) {\n-    if (optimistic) {\n-        // OPTIMISTIC UI CODE\n-        compensateForLatency(channels, collectionName, Events.INSERT, {\n-            _id: docId\n-        });\n-    }\n \n-    Meteor.defer(() => {\n-        publish(this._name, channels, {\n-            [RedisPipe.EVENT]: Events.INSERT,\n-            [RedisPipe.DOC]: { _id: docId }\n-        });\n+const dispatchUpdate = function (optimistic, collectionName, channels, docIds, fields) {\n+    const fence = getWriteFence(optimistic);\n+    const uid = fence ? RedisSubscriptionManager.uid : null;\n+    const events = docIds.map(docId => {\n+        const event = {\n+            [RedisPipe.EVENT]: Events.UPDATE,\n+            [RedisPipe.FIELDS]: fields,\n+            [RedisPipe.DOC]: { _id: docId },\n+            [RedisPipe.UID]: uid,\n+        };\n+        const dedicatedChannel = `${collectionName}::${docId}`;\n+        return { event, dedicatedChannel };\n+    });\n+    dispatchEvents(fence, events, channels);\n+};\n+\n+const dispatchRemove = function (optimistic, collectionName, channels, docIds) {\n+    const fence = getWriteFence(optimistic);\n+    const uid = fence ? RedisSubscriptionManager.uid : null;\n+    const events = docIds.map(docId => {\n+        const event = {\n+            [RedisPipe.EVENT]: Events.REMOVE,\n+            [RedisPipe.DOC]: { _id: docId },\n+            [RedisPipe.UID]: uid,\n+        };\n+        const dedicatedChannel = `${collectionName}::${docId}`;\n+        return { event, dedicatedChannel };\n     });\n+    dispatchEvents(fence, events, channels);\n+};\n+\n+const dispatchInsert = function (optimistic, collectionName, channels, docId) {\n+    const fence = getWriteFence(optimistic);\n+    const uid = fence ? RedisSubscriptionManager.uid : null;\n+    const event = {\n+        [RedisPipe.EVENT]: Events.INSERT,\n+        [RedisPipe.DOC]: { _id: docId },\n+        [RedisPipe.UID]: uid,\n+    };\n+    const dedicatedChannel = `${collectionName}::${docId}`;\n+    const events = [{ event, dedicatedChannel }];","path":"lib/mongo/lib/dispatchers.js","position":145,"original_position":145,"commit_id":"1d8f2c57113629acb241d60aedb09c43a427dcc8","original_commit_id":"1d8f2c57113629acb241d60aedb09c43a427dcc8","user":{"login":"theodorDiaconu","id":1814863,"avatar_url":"https://avatars0.githubusercontent.com/u/1814863?v=4","gravatar_id":"","url":"https://api.github.com/users/theodorDiaconu","html_url":"https://github.com/theodorDiaconu","followers_url":"https://api.github.com/users/theodorDiaconu/followers","following_url":"https://api.github.com/users/theodorDiaconu/following{/other_user}","gists_url":"https://api.github.com/users/theodorDiaconu/gists{/gist_id}","starred_url":"https://api.github.com/users/theodorDiaconu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/theodorDiaconu/subscriptions","organizations_url":"https://api.github.com/users/theodorDiaconu/orgs","repos_url":"https://api.github.com/users/theodorDiaconu/repos","events_url":"https://api.github.com/users/theodorDiaconu/events{/privacy}","received_events_url":"https://api.github.com/users/theodorDiaconu/received_events","type":"User","site_admin":false},"body":"seems a little bit inconsistent to put dedicatedChannel in event and keep channels as a separate array. How do you see if we use a Symbol to store dedicatedChannel inside the Event itself ? Since, in a way, every event contains it's own dedicated channel.","created_at":"2018-03-23T05:41:52Z","updated_at":"2018-03-23T05:41:52Z","html_url":"https://github.com/cult-of-coders/redis-oplog/pull/260#discussion_r176643253","pull_request_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/comments/176643253"},"html":{"href":"https://github.com/cult-of-coders/redis-oplog/pull/260#discussion_r176643253"},"pull_request":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260"}}},"pull_request":{"url":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260","id":176969279,"html_url":"https://github.com/cult-of-coders/redis-oplog/pull/260","diff_url":"https://github.com/cult-of-coders/redis-oplog/pull/260.diff","patch_url":"https://github.com/cult-of-coders/redis-oplog/pull/260.patch","issue_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/issues/260","number":260,"state":"open","locked":false,"title":"Optimistic UI with Meteor's internals.","user":{"login":"nathan-muir","id":3112329,"avatar_url":"https://avatars1.githubusercontent.com/u/3112329?v=4","gravatar_id":"","url":"https://api.github.com/users/nathan-muir","html_url":"https://github.com/nathan-muir","followers_url":"https://api.github.com/users/nathan-muir/followers","following_url":"https://api.github.com/users/nathan-muir/following{/other_user}","gists_url":"https://api.github.com/users/nathan-muir/gists{/gist_id}","starred_url":"https://api.github.com/users/nathan-muir/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nathan-muir/subscriptions","organizations_url":"https://api.github.com/users/nathan-muir/orgs","repos_url":"https://api.github.com/users/nathan-muir/repos","events_url":"https://api.github.com/users/nathan-muir/events{/privacy}","received_events_url":"https://api.github.com/users/nathan-muir/received_events","type":"User","site_admin":false},"body":"@theodorDiaconu This patch leverages some of Meteor's optimistic UI internals.\r\n\r\nThere should probably be some tests, etc, but I thought I'd put this up early for discussion.\r\n\r\n\r\n**How Meteor's Optimistic UI Works**\r\n\r\nBasics:\r\n - Each method call is allocated a `WriteFence` on the server.\r\n - When the method completes, the `WriteFence.arm()` is called. \r\n - At some point later the write fence `fires`, the method call will send the DDP `{updated, msg: [method.id]}` response. Telling clients that \"all data modified during this method has been sent to this client\"\r\n\r\n`Mongo.Collection`:\r\n - Every insert/update/delete performs two extra actions\r\n    - a pair of `fence = WriteFence.beginWrite()` and `fence.committed()`\r\n    - `Meteor.refresh(notification)`\r\n\r\n`Meteor.refresh` aka `DDPServer._InvalidationCrossbar`:\r\n - Notifications sent by mutations on `Mongo.Collection`\r\n - Notifications received by `OplogObserveDriver` and `PollingObserveDriver`\r\n - Only two types of notifications\r\n    - `{collection: 'users'}` to tell all Observers on `Meteor.users` that a mutation happened.\r\n    - `{collection: 'users', id: ... }` to tell an Observer watching that specific `id` that a mutation happened.\r\n\r\n\r\n`OplogObserveDriver` and `PollingObserveDriver`:\r\n - When they receive a Crossbar notification, attach themselves to the current WriteFence.\r\n - Prevent WriteFence's from \"firing\" until observer has caught up & sent all updates to users.\r\n\r\nThe net effect, in pseudo-code could be modelled like \r\n\r\n```js\r\nconst writesPerformed = Mongo.getWritesPerformed();\r\nPromise.all(writesPerformed).then(writes => {\r\n  const observers = new Set()\r\n  writes.forEach(write => observers.addAll(observersWhoAreListening(write)))\r\n  const allUpdated = observers.map(o => o.waitUntilUpdated())\r\n  return Promise.all(allUpdated)\r\n}).then( ()=> this.sendMethodResultsSent())\r\n```\r\n\r\n\r\n\r\n**How to apply to `redis-oplog`**\r\n\r\nGiven all the complications with Meteor's internals, you'd think it would be tricky.\r\n\r\nHowever, `redis-oplog`'s main improvement is getting rid of `DDPServer._InvalidationCrossbar` and replacing it with channels. (Crossbar was not very fine grained!)\r\n\r\nThe only thing we need to do, is hold a `WriteFence` open, until each optimistic change is completed & propagated to the relevant subscribers.\r\n\r\n**These Changes**\r\n\r\n - Some refactoring of `dispatchers.js` to make adding the changes cleaner\r\n - An implementation of optimistic UI, but that still requires duplicate processing of events\r\n - A \"publisher-id\" system to prevent optimistic updates being processed twice.\r\n     - if we know an event is going to be handled immediately (optimistically), add the publishers `uid` to the message\r\n     - when we receive a message via redis with the publishers `uid` == ours... ignore it","created_at":"2018-03-23T04:10:06Z","updated_at":"2018-03-23T05:41:52Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0647859e489ff3e505484f9c63f2ffa6132826c5","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260/commits","review_comments_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260/comments","review_comment_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/comments{/number}","comments_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/issues/260/comments","statuses_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/statuses/1d8f2c57113629acb241d60aedb09c43a427dcc8","head":{"label":"nathan-muir:nm-optimistic-ui","ref":"nm-optimistic-ui","sha":"1d8f2c57113629acb241d60aedb09c43a427dcc8","user":{"login":"nathan-muir","id":3112329,"avatar_url":"https://avatars1.githubusercontent.com/u/3112329?v=4","gravatar_id":"","url":"https://api.github.com/users/nathan-muir","html_url":"https://github.com/nathan-muir","followers_url":"https://api.github.com/users/nathan-muir/followers","following_url":"https://api.github.com/users/nathan-muir/following{/other_user}","gists_url":"https://api.github.com/users/nathan-muir/gists{/gist_id}","starred_url":"https://api.github.com/users/nathan-muir/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nathan-muir/subscriptions","organizations_url":"https://api.github.com/users/nathan-muir/orgs","repos_url":"https://api.github.com/users/nathan-muir/repos","events_url":"https://api.github.com/users/nathan-muir/events{/privacy}","received_events_url":"https://api.github.com/users/nathan-muir/received_events","type":"User","site_admin":false},"repo":{"id":125346864,"name":"redis-oplog","full_name":"nathan-muir/redis-oplog","owner":{"login":"nathan-muir","id":3112329,"avatar_url":"https://avatars1.githubusercontent.com/u/3112329?v=4","gravatar_id":"","url":"https://api.github.com/users/nathan-muir","html_url":"https://github.com/nathan-muir","followers_url":"https://api.github.com/users/nathan-muir/followers","following_url":"https://api.github.com/users/nathan-muir/following{/other_user}","gists_url":"https://api.github.com/users/nathan-muir/gists{/gist_id}","starred_url":"https://api.github.com/users/nathan-muir/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nathan-muir/subscriptions","organizations_url":"https://api.github.com/users/nathan-muir/orgs","repos_url":"https://api.github.com/users/nathan-muir/repos","events_url":"https://api.github.com/users/nathan-muir/events{/privacy}","received_events_url":"https://api.github.com/users/nathan-muir/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/nathan-muir/redis-oplog","description":"Redis Oplog implementation to fully replace MongoDB Oplog in Meteor","fork":true,"url":"https://api.github.com/repos/nathan-muir/redis-oplog","forks_url":"https://api.github.com/repos/nathan-muir/redis-oplog/forks","keys_url":"https://api.github.com/repos/nathan-muir/redis-oplog/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nathan-muir/redis-oplog/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nathan-muir/redis-oplog/teams","hooks_url":"https://api.github.com/repos/nathan-muir/redis-oplog/hooks","issue_events_url":"https://api.github.com/repos/nathan-muir/redis-oplog/issues/events{/number}","events_url":"https://api.github.com/repos/nathan-muir/redis-oplog/events","assignees_url":"https://api.github.com/repos/nathan-muir/redis-oplog/assignees{/user}","branches_url":"https://api.github.com/repos/nathan-muir/redis-oplog/branches{/branch}","tags_url":"https://api.github.com/repos/nathan-muir/redis-oplog/tags","blobs_url":"https://api.github.com/repos/nathan-muir/redis-oplog/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nathan-muir/redis-oplog/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nathan-muir/redis-oplog/git/refs{/sha}","trees_url":"https://api.github.com/repos/nathan-muir/redis-oplog/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nathan-muir/redis-oplog/statuses/{sha}","languages_url":"https://api.github.com/repos/nathan-muir/redis-oplog/languages","stargazers_url":"https://api.github.com/repos/nathan-muir/redis-oplog/stargazers","contributors_url":"https://api.github.com/repos/nathan-muir/redis-oplog/contributors","subscribers_url":"https://api.github.com/repos/nathan-muir/redis-oplog/subscribers","subscription_url":"https://api.github.com/repos/nathan-muir/redis-oplog/subscription","commits_url":"https://api.github.com/repos/nathan-muir/redis-oplog/commits{/sha}","git_commits_url":"https://api.github.com/repos/nathan-muir/redis-oplog/git/commits{/sha}","comments_url":"https://api.github.com/repos/nathan-muir/redis-oplog/comments{/number}","issue_comment_url":"https://api.github.com/repos/nathan-muir/redis-oplog/issues/comments{/number}","contents_url":"https://api.github.com/repos/nathan-muir/redis-oplog/contents/{+path}","compare_url":"https://api.github.com/repos/nathan-muir/redis-oplog/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nathan-muir/redis-oplog/merges","archive_url":"https://api.github.com/repos/nathan-muir/redis-oplog/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nathan-muir/redis-oplog/downloads","issues_url":"https://api.github.com/repos/nathan-muir/redis-oplog/issues{/number}","pulls_url":"https://api.github.com/repos/nathan-muir/redis-oplog/pulls{/number}","milestones_url":"https://api.github.com/repos/nathan-muir/redis-oplog/milestones{/number}","notifications_url":"https://api.github.com/repos/nathan-muir/redis-oplog/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nathan-muir/redis-oplog/labels{/name}","releases_url":"https://api.github.com/repos/nathan-muir/redis-oplog/releases{/id}","deployments_url":"https://api.github.com/repos/nathan-muir/redis-oplog/deployments","created_at":"2018-03-15T09:56:16Z","updated_at":"2018-03-15T09:56:17Z","pushed_at":"2018-03-23T04:45:38Z","git_url":"git://github.com/nathan-muir/redis-oplog.git","ssh_url":"git@github.com:nathan-muir/redis-oplog.git","clone_url":"https://github.com/nathan-muir/redis-oplog.git","svn_url":"https://github.com/nathan-muir/redis-oplog","homepage":null,"size":439,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"cult-of-coders:release/1.2.7","ref":"release/1.2.7","sha":"3b21e9463ca1f51a1e5cad76a5be09353e69ae7e","user":{"login":"cult-of-coders","id":18182150,"avatar_url":"https://avatars2.githubusercontent.com/u/18182150?v=4","gravatar_id":"","url":"https://api.github.com/users/cult-of-coders","html_url":"https://github.com/cult-of-coders","followers_url":"https://api.github.com/users/cult-of-coders/followers","following_url":"https://api.github.com/users/cult-of-coders/following{/other_user}","gists_url":"https://api.github.com/users/cult-of-coders/gists{/gist_id}","starred_url":"https://api.github.com/users/cult-of-coders/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cult-of-coders/subscriptions","organizations_url":"https://api.github.com/users/cult-of-coders/orgs","repos_url":"https://api.github.com/users/cult-of-coders/repos","events_url":"https://api.github.com/users/cult-of-coders/events{/privacy}","received_events_url":"https://api.github.com/users/cult-of-coders/received_events","type":"Organization","site_admin":false},"repo":{"id":72449523,"name":"redis-oplog","full_name":"cult-of-coders/redis-oplog","owner":{"login":"cult-of-coders","id":18182150,"avatar_url":"https://avatars2.githubusercontent.com/u/18182150?v=4","gravatar_id":"","url":"https://api.github.com/users/cult-of-coders","html_url":"https://github.com/cult-of-coders","followers_url":"https://api.github.com/users/cult-of-coders/followers","following_url":"https://api.github.com/users/cult-of-coders/following{/other_user}","gists_url":"https://api.github.com/users/cult-of-coders/gists{/gist_id}","starred_url":"https://api.github.com/users/cult-of-coders/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cult-of-coders/subscriptions","organizations_url":"https://api.github.com/users/cult-of-coders/orgs","repos_url":"https://api.github.com/users/cult-of-coders/repos","events_url":"https://api.github.com/users/cult-of-coders/events{/privacy}","received_events_url":"https://api.github.com/users/cult-of-coders/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/cult-of-coders/redis-oplog","description":"Redis Oplog implementation to fully replace MongoDB Oplog in Meteor","fork":false,"url":"https://api.github.com/repos/cult-of-coders/redis-oplog","forks_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/forks","keys_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/teams","hooks_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/hooks","issue_events_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/issues/events{/number}","events_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/events","assignees_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/assignees{/user}","branches_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/branches{/branch}","tags_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/tags","blobs_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/git/refs{/sha}","trees_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/statuses/{sha}","languages_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/languages","stargazers_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/stargazers","contributors_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/contributors","subscribers_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/subscribers","subscription_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/subscription","commits_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/commits{/sha}","git_commits_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/git/commits{/sha}","comments_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/comments{/number}","issue_comment_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/issues/comments{/number}","contents_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/contents/{+path}","compare_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/merges","archive_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/downloads","issues_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/issues{/number}","pulls_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls{/number}","milestones_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/milestones{/number}","notifications_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/labels{/name}","releases_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/releases{/id}","deployments_url":"https://api.github.com/repos/cult-of-coders/redis-oplog/deployments","created_at":"2016-10-31T15:22:42Z","updated_at":"2018-03-22T16:24:23Z","pushed_at":"2018-03-23T04:45:39Z","git_url":"git://github.com/cult-of-coders/redis-oplog.git","ssh_url":"git@github.com:cult-of-coders/redis-oplog.git","clone_url":"https://github.com/cult-of-coders/redis-oplog.git","svn_url":"https://github.com/cult-of-coders/redis-oplog","homepage":null,"size":447,"stargazers_count":204,"watchers_count":204,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":20,"mirror_url":null,"archived":false,"open_issues_count":24,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit"},"forks":20,"open_issues":24,"watchers":204,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260"},"html":{"href":"https://github.com/cult-of-coders/redis-oplog/pull/260"},"issue":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/issues/260"},"comments":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/issues/260/comments"},"review_comments":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260/comments"},"review_comment":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/pulls/260/commits"},"statuses":{"href":"https://api.github.com/repos/cult-of-coders/redis-oplog/statuses/1d8f2c57113629acb241d60aedb09c43a427dcc8"}},"author_association":"NONE"}}
{"actor":{"display_login":"theodorDiaconu"}}