• This repository has been archived on 18/Feb/2021
  • Stars
    star
    333
  • Rank 126,599 (Top 3 %)
  • Language
    Swift
  • License
    MIT License
  • Created about 10 years ago
  • Updated almost 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

An elegant model framework written in Swift

Jetstream: Elegant MVC in Swift

Build Status

Jetstream for iOS is an elegant MVC model framework written in Swift. It includes support for the Jetstream Sync protocol to sync local and remote models. Out of the box, it has a single Websocket transport adapter with the ability to add custom transport adapters.

Features

  • Change observation
  • Fire-and-forget observation
  • Synchronization protocol to create multi-user applications in minutes
  • Modular architecture
  • Comprehensive Unit Test Coverage
  • Works well together with Objective-C

Requirements

  • iOS 7.0+ / Mac OS X 10.9+
  • Xcode 7.0
  • Swift 2.0

Installation

  1. Add Jetstream as a submodule: git submodule add https://github.com/uber/jetstream-ios.git
  2. Open the Jetstream folder, and drag Jetstream.xcodeproj into the project navigator of your app.
  3. In Xcode, select your project, navigate to the General tab and click the + - icon in the "Embedded Binaries" section. Select Jetstream.framework.

Quick start

Jetstream works with two basic concepts: All your model objects extend from the superclass ModelObject and one of your ModelObject instances will be the root for your model tree which is wrapped by a Scope.

Let's model a canvas of shapes:

public class Shape: ModelObject {
    dynamic var x: Float = 0
    dynamic var y: Float = 0
    dynamic var width: Float = 100
    dynamic var height: Float = 100
    dynamic var color: UIColor = UIColor.redColor()
}

public class Canvas: ModelObject {
    dynamic var name: String?
    dynamic var shapes = [Shape]()
}

Once you've defined your model classes, instantiate a canvas and mark it as a scope root.

var canvas = Canvas()

var scope = Scope(name: "Canvas")
scope.root = canvas

This will create a new scope and assign canvas as the root of the scope. The root object or any branches or leafs attached now belong to the scope. This lets you start observing changes happening to any models that have been attached to the tree:

class CanvasViewController: UIViewController {
    …
    var model: Canvas

    func init() {
        canvas.observeCollectionAdd(self, key: "shapes") { (element: Shape) in
            // A new shape was just added to our shapes-collection.
            view.addChild(ShapeView(shape: element))        
        }
    }
}

class ShapeView: UIView {
    …
    init(shape: Shape) {
        self.shape = shape
        shape.observeChange(self, keys: ["x", "y", "width", "height"]) {
            self.frame = {{shape.x, shape.y}, {shape.width, shape.height}}
        }
        shape.observeChange(self, key: "color") {
            self.backgroundColor = shape.color
        }
        shape.observeDetach(self) {
            // The shape model instance has been removed from the scope
            removeFromParentView()
        }
    }
}

This is all that is needed to create an application that binds itself to a view and works no matter how your model is changed. In fact, if you use Jetstreams built-in Websocket support to connect to the Jetstream server (OOS release to follow), changes coming in from remote users would update your UI perfectly without any changes to the code.

This is in essence how Jetstream works. You define a classes that model the data of your application. Your ViewControllers and Views then observe changes on the model to update their UI. In our canvas of shapes example, our ViewController listens to changes on the shapes-collection. Whenever a shape is added, it creates a ShapeView instance, adds it as a child view, and passes the Shape model to it. The ShapeView will bind to all the properties of the Shape and update its frame whenever. The ShapeView also observes whenever the Shape is detached (i.e. removed from the shapes-collection) and removes it from its parent view.

Usage

Models

You create a model by subclassing ModelObject and defining properties of the model as dynamic variables. Getters, private variables and constants are not observed by the model.

Supported types are String, UInt, Int, Float, Double, Bool, ModelObject, [ModelObject], UIColor and NSDate, UInt8, Int8, UInt16, Int16, UInt32, Int32

As Jetstream relies on Objective-C runtime to detect changes to properties, only types that can be represented in Objective-C can be used as property types. Unfortunately Swift enums cannot be represented in Objective-C and thus cannot be used. To use enums types, declare them in an Objective-C header file.

Observation

You have a number of methods to observe changes on model objects. Jetstream uses Signals for all of its events. While you can subscribe to a number of signals that fire whenever changes occur on a model object, Jetstream provides various observer methods that wrap around these signals to provide queueing and a cleaner interface.

// Observe property changes on models
model.observeChange(self) { ... } // Observe all changes
model.observeChange(self, key: "width") { ... } // Observe a single property
model.observeChange(self, keys: ["width", "height"]) { ... } // Observe a set of properties

All of these methods queue up their notifications and fire only one time per run-loop. This is usually the behavior you want. When updating your view whenever properties change, you usually don't want to run the view update code for every single property change, but do an update whenever all of the changes have been applied. For example, if the elements width and height properties have both changed, all of the observers will only fire off once in the next run-loop.

In some cases you might actually the change observers to fire off the callback for every single property change, which you can do using the immediate variants of the change observers:

// Observe property changes on models without queueing them up
model.observeChangeImmediately(self) { ... }
model.observeChangeImmediately(self, key: "width") { ... }
model.observeChangeImmediately(self, keys: ["width", "height"]) { ... }

Whenever collections change, they fire off the property change observers, but they also fire off two other observers:

// Observe collection changes
model.observeCollectionAdd(self, key: "collection") { (element: ElementType) in ... }
model.observeCollectionRemove(self, key: "collection") { (element: ElementType) in ... }

Callbacks fire immediately whenever an element is added or removed from the collection and the callbacks receive the element as an argument.

A very powerful feature is the ability to observe changes in the properties of a model object or any of it's children and its children's children. This is useful when your UI needs to re-render itself when a model object or any of its children change.

// Observe changes of an entire model object and its children
model.observeTreeChange(self) { ... } // Tree has changed

Tree changes are always queued up as they would otherwise degrade performance.

Usually you're also interested whenever a model object is attached or detached from a scope. A model object is attached to a scope whenever it is inserted somewhere in a tree of model objects that have a scope. This might mean it's been added to a collection or assigned to a property of a parent model object that is part of a scope. A model object is detached when it loses its last parent with a scope (again, this might occur when the model object is removed from a collection or when the property of a parent model object is nilled out).

// Observe scope attachments and detachments
model.observeAttach(self) { (scope: Scope) in ... } // Attached to a scope
model.observeDettach(self) { (scope: Scope) in ... } // Detached to a scope

Attach and detach events traverse through all the children of the model object, so when you create a tree of model objects that have no scope and then attach the root of that tree to a parent with a scope, the whole tree would fire its attach observers.

Related, but slightly different is observing changes in parenting. Each model object with a scope has at least one parent (except for the root node), identified by a parent model object and a key which is the name of either the property or collection to which it has been attached to. A model object can have multiple parents. It might be mounted to properties of multiple parents, or it might be mounted on multiple different keys of the same parent model object. Whenever any changes in their parent relationships occur, you can observe these changes.

// Observe scope attachments and detachments

// Added to a parent
model.observeAddedToParent(self) { (parent: ModelObject, key: String) in ... }
// Removed from a parent
model.observeRemovedFromParent(self) { (parent: ModelObject, key:String) in ... }

To unsubscribe from events you can either call model.removeObserver(listener) to remove all observations for a given listener, or you can use the function returned by all of the observer methods to cancel that specific observation:

// Cancel a single observation
var cancelObservation = model.observeTreeChange(self) { ... }
...
cancelObservation() // Cancels the observation

Scope

Reading changes from a scope

A scope wraps around a model tree and keeps tabs on what models have been added to the tree. It lets you access all models in the tree by UUID and lets you listen to changes that happen to all of your models as a digest of sync fragments. Sync fragments represent changes to the models in the scope and they come in two types:

  • Add: Adds a new model to the scope
  • Change: Updates properties of an existing model

There is no remove type as model objects are never removed explicitly, but they no longer have any parents.

When you make changes to models in a scope, add new models by assigning them to properties or collections or remove models by removing them from collections by setting the property under which they are mounted to the tree to nil, the scope registers these changes and combines them to a number of sync fragments. You can listen to these changes by listening to the onChanges signal on the scope:

// Listening to changes
scope.onChanges.listen(self) { fragments in
    // fragments is an Array of SyncFragments that describe the changes that happened
}

The onChanges signal fires whenever changes have been made to models in the tree. It queues up changes for a fraction of a second and delivers them all at once. The scope is intelligent enough to combine subsequent changes and deliver only required fragments. For example, if you add a model to a tree (resulting in an Add fragment) and immediately remove it from the tree (resulting in a Remove fragment), both fragments will cancel themselves out and neither one will be delivered on the onChanges signal.

Atomic ChangeSets

The Scope provides a mechanism to apply a number of changes as a atomic operation. Atomic changes are either accepted in full by the server or rejected as a whole. This is useful if you for example add an item to a checkout cart and update the total price of your cart. You wouldn't either of these changes be applied to the model without the other one.

// Creating atomic transactions
scope.createAtomicChangeSet() {
    self.cart.items.append(newItem)
    self.cart.totalPrice += newItem.price
}

Applying changes to a scope

With the onChanges signal you can easily detect changes that happen on your local model. But you can also apply sync fragments to update your local model:

// Apply sync fragments
var fragments = [SyncFragments]()
...
scope.applySyncFragments(fragments) // Applies the changes to your model

Since SyncFragments can serialize themselves to a JSON-serializable dictionary using syncFragment.serialize() and unserialize themselves from a dictionary using SyncFragment.unserialize(dictionary), you have an easy tool to build many kinds of extensions to Jetstream. For example, you could easily persist all the changes made to a scope by writing all sync fragments to disk, and on startup restore the previous state by reading the data from disk and apply them to the scope.

There's one particular built-in extension that makes great use of this functionality: Synchronization.

##Synchronization Jetstream comes with a powerful synchronization mechanism that lets you create multi-user applications in minutes:

// Synchronizing a scope
scope = new Scope("ShapesCanvas")
if let client = Client(options: WebsocketConnectionOptions(url: "ws://localhost")) {
    client.connect()
    client.onSession.listenOnce(self) { (session) in
        self.session = session
        session.fetch(scope) { (error) in
            if error == nil {
                // Registered to receive updates to the scope from the Jetstream server
            }
        }
    }
}

This creates a scope and a client, using the built-in Websocket transport adapter. Once successfully connected to the Jetstream server running on localhost, we ask our session to fetch the initial state of our scope. This will have the Jetstream server send us the full model tree for the scope and start keeping us in sync with remote changes as well as transmit changes that we make locally to our model up to the remote scope.

This is all you need to do create a multi-user data model. When any remote clients connected to the same scope make changes to their local models, Jetstream will synchronize these changes in a efficient manner (sending a delta of these changes as sync fragments) with our local model and these changes will automatically be applied to our model and observers will fire, updating our views. Local changes made by our client will also be automatically synchronized with the remote scope and any remote clients will receive the changes we make to our model in real-time.

Protocol

Jetstream uses JSON-based messages to create sessions, fetch scopes and synchronize changes. In case you want to build your own client or server, refer to the protocol documentation.

Communication

  • If you found a bug, open an issue or submit a fix via a pull request.
  • If you have a feature request, open an issue or submit a implementation via a pull request.
  • If you want to contribute, submit a pull request.

License

Jetstream is available under the MIT license. See the LICENSE file for more info.

More Repositories

1

go-torch

Stochastic flame graph profiler for Go programs
Go
3,958
star
2

pyflame

🔥 Pyflame: A Ptracing Profiler For Python. This project is deprecated and not maintained.
C++
2,974
star
3

image-diff

Create image differential between two images
JavaScript
2,453
star
4

makisu

Fast and flexible Docker image building tool, works in unprivileged containerized environments like Mesos and Kubernetes.
Go
2,409
star
5

cpustat

high frequency performance measurements for Linux. This project is deprecated and not maintained.
Go
1,659
star
6

cherami-server

Distributed, scalable, durable, and highly available message queue system. This project is deprecated and not maintained.
Go
1,416
star
7

AthenaX

SQL-based streaming analytics platform at scale
Java
1,224
star
8

plato-research-dialogue-system

This is the Plato Research Dialogue System, a flexible platform for developing conversational AI agents.
Python
977
star
9

npm-shrinkwrap

A consistent shrinkwrap tool
JavaScript
775
star
10

chaperone

A Kafka audit system
Java
640
star
11

coding-challenge-tools

Uber's tools team coding challenge
562
star
12

hyperbahn

Service discovery and routing for large scale microservice operations
JavaScript
394
star
13

sql-differential-privacy

Dataflow analysis & differential privacy for SQL queries. This project is deprecated and not maintained.
Scala
391
star
14

phabricator-jenkins-plugin

Jenkins plugin to integrate with Phabricator, Harbormaster, and Uberalls
Java
367
star
15

ohana-ios

Contacts simplified. This project is deprecated and not maintained.
Objective-C
362
star
16

rave

A data model validation framework that uses java annotation processing.
Java
355
star
17

node-stap

Tools for analyzing Node.js programs with SystemTap. This project is deprecated and not maintained.
JavaScript
291
star
18

r-dom

React DOM wrapper
JavaScript
263
star
19

focuson

A tool to surface security issues in python code
Python
226
star
20

cherami-client-go

Go Client Implementation of Cherami - A distributed, scalable, durable, and highly available message queue system. This project is deprecated and not maintained.
Go
207
star
21

viewport-mercator-project

NOTE: The viewport-mercator-project repo is archived and code has moved to
JavaScript
137
star
22

infer-plugin

Gradle plugin that allows easy integration with the infer static analyzer.
Groovy
126
star
23

express-statsd

Statsd route monitoring middleware for connect/express
JavaScript
126
star
24

android-build-environment

Docker repository for android build environment
122
star
25

in-n-out

A library to perform point-in-geofence searches.
JavaScript
106
star
26

buck-http-cache

An Implementation of Buck's HTTP Cache API as a distributed cache service. This project is deprecated and not maintained.
Shell
101
star
27

statsrelay

A consistent-hashing relay for statsd and carbon metrics
C
101
star
28

hacheck

HAproxy healthcheck proxying service
Python
86
star
29

potter

a CLI to create node.js services
JavaScript
83
star
30

opentracing-go

A general-purpose instrumentation API for distributed tracing systems
Go
82
star
31

idl

A CLI for managing Thrift IDL files
JavaScript
78
star
32

jetstream

Jetstream Sync server framework
JavaScript
73
star
33

canduit

Node.js Phabricator Conduit API client. This project is deprecated and not maintained.
JavaScript
65
star
34

kafka-spraynozzle

A nozzle to spray a kafka topic at an HTTP endpoint. This project is deprecated and not maintained.
Java
49
star
35

usb2fac

Enabling 2fac confirmation for newly connected USB devices
Python
44
star
36

nanny

Cluster management for Node processes
JavaScript
40
star
37

auto-value-bundle

Extends Autovalue to extract data from a bundle into a value object.
Java
36
star
38

node-flame

Tools for analyzing Node.js programs with ptrace. This project is deprecated and not maintained.
JavaScript
29
star
39

Bug-Bounty-Page

A repo to make our changes more transparent to bug bounty researchers in our program (so they can see commits, etc).
29
star
40

paranoid-request

An SSRF-preventing wrapper around Node's request module
JavaScript
26
star
41

lint-trap

JavaScript linter module for Uber projects
JavaScript
26
star
42

thriftify

JavaScript implementation of Thrift encoding and decoding
JavaScript
25
star
43

HackerOneAlchemy

A tool to generate statistics and help manage bug bounty reports in HackerOne.
Python
23
star
44

express-translate

Add simple translation support to Express
JavaScript
21
star
45

cherami-thrift

Thrift APIs for Cherami - A distributed, scalable, durable, and highly available message queue system. This project is deprecated and not maintained.
Go
20
star
46

h1-python

A HackerOne API client for Python
Python
19
star
47

cidrtrie

Trie implementation of a CIDR lookup table
Python
19
star
48

ios-template

This template provides a starting point for open source iOS projects at Uber.
Ruby
18
star
49

tcheck

TChannel health check utility
Go
17
star
50

job_progress

Store the progress of a job
Python
16
star
51

java-code-styles

IntelliJ IDEA code style settings for Uber's Java and Android projects.
15
star
52

fixed-server

Server for HTTP fixtures
JavaScript
14
star
53

vis-academy

A set of tutorials on how our frameworks make effective data visualization applications.
JavaScript
13
star
54

shared-docs

Shared Markdown Documents from Uber Engineering
12
star
55

typed-request-stack

Middleware stack runner for typed HTTP requests
JavaScript
11
star
56

cherami-client-python

Python Client for Cherami - A distributed, scalable, durable, and highly available message queue system. This project is deprecated and not maintained.
Python
11
star
57

failpointsjs

JavaScript
10
star
58

instafork

JavaScript
8
star
59

py-find-unicode

Find incorrect unicode() invocations
Python
8
star
60

shallow-settings

Shallow inheritance-based settings for your application
JavaScript
7
star
61

clusto-query

Silly CLI for querying clusto more quickly
Python
7
star
62

gg

Go dependency debugger
Go
7
star
63

connect-csrf-lite

CSRF validation middleware for Connect/Express
JavaScript
7
star
64

javax-extras

(DEPRECATED) Extra utilities for javax
Java
6
star
65

fixtures-fs

Create a temporary fs with JSON fixtures
JavaScript
6
star
66

redis-delete-pattern

Delete a set of keys from a pattern in Redis
6
star
67

opentracing-python

NOTE: This repository has been retired. The latest OpenTracing APIs can be found in the official repository.
Python
5
star
68

tchannel-gen

Scaffolding for new TChannel w/ Hyperbahn applications
JavaScript
5
star
69

node-dot-arcanist

Uber's .arcanist folder as an npm module
PHP
5
star
70

cherami-client-java

Java Client for Cherami. This project is deprecated and not maintained.
Java
5
star
71

pyrehol

Python wrapper for Firehol
Python
4
star
72

dubstep

This repo is DEPRECATED. See https://github.com/dubstepjs/core
JavaScript
4
star
73

ottr

Easy, robust end-to-end UI tests for web apps
JavaScript
3
star
74

clouseau

A Node.js performance profiler by Uber
JavaScript
3
star
75

vertica-aesgcm-udx

C++
2
star
76

stacked

Go
2
star
77

request-redis-cache

Make requests and cache them in Redis
JavaScript
2
star
78

nodesol-write

Kafka producer.
JavaScript
2
star
79

request-mocha

Request utilities for Mocha
JavaScript
2
star
80

UberBuilder

Make building flexible, immutable objects a simple task
Objective-C
2
star
81

uLeak

DEPRECATED: This is continued in https://github.com/behroozkhorashadi/uLeak
Java
2
star
82

fusion-orchestrate

Tools and scripts for working across multiple fusion repos at once
JavaScript
2
star
83

deck.gl-data-osm

OSM data for the data visualization library deck.gl examples (https://uber.github.io/deck.gl/#/)
1
star
84

uberclass-clouseau

A subclass of uberclass that adds profiling support
JavaScript
1
star
85

backbone-api-client

Backbone mixin built for interacting with API clients
JavaScript
1
star
86

fusion-release

Releases and verifies FusionJS packages
JavaScript
1
star
87

cache-redis

An ES6 Map-like cache with redis backing
JavaScript
1
star
88

redis-broadcast

Write redis commands to a set of redises efficiently
JavaScript
1
star