Kitura Logo

TYPE-SAFE SESSIONS

Introduction

Kitura 2.4 introduces "Type-safe Middleware": middlewares that have a structure and data types that you define, that are instantiated and passed to your handler upon a request. Type-Safe Sessions is an implementation of TypeSafeMiddleware that allows an application developer to describe the exact structure and content of a session, in the form of a class conforming to the TypeSafeSession protocol.

The key feature is in the name: type safety. The previous implementation of KituraSession that can be used alongside "Raw Routing" provides you with a general [String: Any] store, into which any type could be stored and retrieved. This places the burden of type checking on the user: any value retrieved from the session would need a downcast to its expected type, resulting in boilerplate checking and error handling, and the possibility of runtime errors.

In contrast, TypeSafeSession has been designed to provide the following guarantees:

Adding TypeSafeSession to your project

Add Kitura-Session to your Package.swift dependencies:

    .package(url: "https://github.com/IBM-Swift/Kitura-Session.git", from: "3.2.0"),

Add KituraSession to your Application targets:

    .target(name: "Application", dependencies: [ "KituraSession", ...

Regenerate your Xcode project:

swift package generate-xcodeproj

Defining a TypeSafeSession

To create a TypeSafeSession, declare a type that conforms to the TypeSafeSession protocol:

// Defines the session instance data
final class MySession: TypeSafeSession {

    let sessionId: String                       // Requirement: every session must have an ID
    var books: [Book]                           // User-defined type, where Book conforms to Codable
    
    init(sessionId: String) {                   // Requirement: must be able to create a new (empty)
        self.sessionId = sessionId              // session containing just an ID. Assign a default or
        books = []                              // empty value for any non-optional properties.
    }
}

// Defines the configuration of the user's type: how the cookie is constructed, and how the session is
// persisted.
extension MySession {
    static let sessionCookie: SessionCookie = SessionCookie(name: "MySession", secret: "Top Secret")
    static var store: Store?
}

The minimum requirements for a TypeSafeSession are:

If store is not assigned, then a default in-memory store is used. Kitura-Session-Redis is an example of a persistent store for sessions, which supports expiry.

The MySession type can then be included in the application's Codable route handlers. For example:

struct Book: Codable {
    let title: String
    let author: String
}

router.get("/cart") { (session: MySession, respondWith: ([Book]?, RequestError?) -> Void) -> Void in
    respondWith(session.books, nil)
}

router.post("/cart") { (session: MySession, book: Book, respondWith: (Book?, RequestError) -> Void) -> Void in
    session.books.append(book)
    session.save()
    respondWith(book, nil)
}

Here the cart accepts a book item of type Book, appends the book to the list of books in the session, and responds with the book that was added, or a RequestError.internalServerError if the session could not be saved (such as a failure of the session store).

Note that if you define MySession as a class, it must be marked final.

If you define MySession as a struct, then in order to modify it within a handler, you will first need to assign it to a local variable:

router.post("/cart") { (session: MySessionStruct, book: Book, respondWith: (Book?, RequestError) -> Void) -> Void in
    var session = session                // Required when mutating a Struct
    session.books.append(book)
    session.save()
    respondWith(book, nil)
}

Saving a session

It is necessary to save the session when updates have been made:

    session.save()

Terminating a session

To explicitly terminate a session, removing it from the store, call:

    session.destroy()

Handling store failure

It is possible that the session store could become inaccessible, resulting in a failure to persist or remove sessions from the store. In such cases, an error will be logged for you. However, you can also take additional steps in the case of an error by supplying a callback which takes an Error?:

router.post("/cart") { (session: MySession, book: Book, respondWith: @escaping (Book?, RequestError) -> Void) -> Void in
    session.books.append(book)
    session.save() { error in
        if let error = error {
            respondWith(nil, .internalServerError)
        } else {
            respondWith(book, nil)
        }
    }
}

Automatic saving of sessions

If you have declared MySession as a class, it is possible to implement an automatic saving of the session by defining a deinitializer on the class:

    deinit {
        self.save()
    }

Be aware that the session will then be saved after every request, regardless of whether it has been modified.

You may also want a method of tracking whether destroy() has been called, to suppress calling save() during deinitialization - otherwise, the session will be persisted back into the store again. For example:

    var destroyed: Bool = false {
        didSet { destroy() }
    }

    deinit {
        if !self.destroyed {
            self.save()
        }
    }

Then, you can replace session.destroy() with session.destroyed = true.

Slack icon

NEED HELP?

MESSAGE US ON SLACK.