Skip to content

Commit

Permalink
Add JSScheduler and JSPromise publisher (#1)
Browse files Browse the repository at this point in the history
`JSScheduler` is copied from TokamakUI/Tokamak#281.

You can see the `JSPromise` publisher in action in combination with `fetch` by running `carton dev` in the root directory. I saw some issues with `JSValueDecoder`, which is why it's not used in this PR. I will investigate `JSValueDecoder` issues separately.

* Add `JSScheduler` and `JSPromise` publisher

* Clean up publisher signature in `main.swift`

* Capture `self` weakly in `PromisePublisher.init`
  • Loading branch information
MaxDesiatov authored Sep 30, 2020
1 parent 6bcfd07 commit c52eff4
Show file tree
Hide file tree
Showing 14 changed files with 519 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
linux_build:
runs-on: ubuntu-20.04

steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@master
with:
shell-action: swift build --triple wasm32-unknown-wasi
28 changes: 28 additions & 0 deletions .github/workflows/label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Check PR labels

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the `main` branch
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
check-labels:
# The type of runner that the job will run on
runs-on: ubuntu-latest

steps:
- name: Match PR Label
uses: zwaldowski/match-label-action@v2
with:
allowed_multiple: >
API design,
bug,
continuous integration,
dependencies,
documentation,
enhancement,
refactor,
test suite,
17 changes: 17 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: detect-private-key
- id: check-merge-conflict
- repo: https://github.com/hodovani/pre-commit-swift
rev: master
hooks:
- id: swift-lint
- id: swift-format
12 changes: 12 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--indent 2
--indentcase false
--trimwhitespace always
--voidtype tuple
--nospaceoperators ..<,...
--ifdef noindent
--stripunusedargs closure-only
--maxwidth 100
--wraparguments before-first
--funcattributes prev-line
--disable andOperator
--swiftversion 5.3
19 changes: 19 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
disabled_rules:
- trailing_comma
- identifier_name
- void_return
- operator_whitespace
- nesting
- cyclomatic_complexity
- multiple_closures_with_trailing_closure
- type_name
- opening_brace

line_length: 100

function_body_length:
- 50

included:
- Sources
- Tests
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"editor.tabSize": 2,
"editor.formatOnSave": true,
"licenser.author": "OpenCombineJS contributors"
}
12 changes: 12 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "carton dev",
"type": "shell",
"command": "carton dev"
}
]
}
34 changes: 34 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"object": {
"pins": [
{
"package": "JavaScriptKit",
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit.git",
"state": {
"branch": null,
"revision": "6e84a7003071ed3e9fa7f8d8266a272a36b84608",
"version": "0.7.2"
}
},
{
"package": "OpenCombine",
"repositoryURL": "https://github.com/MaxDesiatov/OpenCombine.git",
"state": {
"branch": null,
"revision": "29ab27e488a1c9afef217b1435b3293d4a77b1ae",
"version": "0.0.1"
}
},
{
"package": "Runtime",
"repositoryURL": "https://github.com/MaxDesiatov/Runtime.git",
"state": {
"branch": null,
"revision": "a617ead8a125a97e69d6100e4d27922006e82e0a",
"version": "2.1.2"
}
}
]
},
"version": 1
}
35 changes: 35 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "OpenCombineJS",
products: [
.executable(name: "OpenCombineJSExample", targets: ["OpenCombineJSExample"]),
.library(name: "OpenCombineJS", targets: ["OpenCombineJS"]),
],
dependencies: [
.package(
name: "JavaScriptKit",
url: "https://github.com/swiftwasm/JavaScriptKit.git",
from: "0.7.2"
),
.package(
name: "OpenCombine",
url: "https://github.com/MaxDesiatov/OpenCombine.git",
from: "0.0.1"
),
],
targets: [
.target(
name: "OpenCombineJSExample",
dependencies: [
"OpenCombineJS",
]
),
.target(
name: "OpenCombineJS",
dependencies: [
"JavaScriptKit", "OpenCombine",
]
),
]
)
22 changes: 22 additions & 0 deletions Sources/OpenCombineJS/JSError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2020 OpenCombineJS contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import JavaScriptKit

extension JSError: JSValueConstructible {
public static func construct(from value: JSValue) -> JSError? {
guard let object = value.object else { return nil }
return JSError(unsafelyWrapping: object)
}
}
86 changes: 86 additions & 0 deletions Sources/OpenCombineJS/JSPromise.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2020 OpenCombineJS contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import JavaScriptKit
import OpenCombine

extension JSPromise where Success: JSValueConstructible, Failure: JSError {
public final class PromisePublisher: Publisher {
public typealias Output = Success

/// Reference to a parent promise instance to prevent early deallocation
private var parent: JSPromise?

/// Reference to a `then` success callback promise instance to prevent early deallocation
private var then: JSPromise<JSValue, Failure>?

/// `Future` instance that handles subscriptions to this publisher.
private var future: Future<Success, Failure>?

fileprivate init(parent: JSPromise) {
future = .init { [weak self] resolver in
let then = parent.then { value -> JSValue in
resolver(.success(value))
return .undefined
}

then.catch {
resolver(.failure($0))
}
self?.then = then
}
self.parent = parent
}

public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Success == Downstream.Input, Failure == Downstream.Failure
{
guard let parent = parent, let then = then, let future = future else { return }

future.receive(subscriber: WrappingSubscriber(inner: subscriber, parent: parent, then: then))
}
}

/// Creates a new publisher for this `JSPromise` instance.
public var publisher: PromisePublisher {
.init(parent: self)
}

/** Helper type that wraps a given `inner` subscriber and holds references to both stored promises
of `PromisePublisher`, as `PromisePublisher` itself can be deallocated earlier than its
subscribers.
*/
private struct WrappingSubscriber<Inner: Subscriber>: Subscriber {
typealias Input = Inner.Input
typealias Failure = Inner.Failure

let inner: Inner
let parent: JSPromise
let then: JSPromise<JSValue, Failure>

var combineIdentifier: CombineIdentifier { inner.combineIdentifier }

func receive(subscription: Subscription) {
inner.receive(subscription: subscription)
}

func receive(_ input: Input) -> Subscribers.Demand {
inner.receive(input)
}

func receive(completion: Subscribers.Completion<Failure>) {
inner.receive(completion: completion)
}
}
}
Loading

0 comments on commit c52eff4

Please sign in to comment.