├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .haxerc ├── .travis.yml.disabled ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── dev.hxml ├── extraParams.hxml ├── haxe_libraries ├── ansi.hxml ├── hxcpp.hxml ├── hxcs.hxml ├── hxjava.hxml ├── hxnodejs.hxml ├── tink_cli.hxml ├── tink_core.hxml ├── tink_io.hxml ├── tink_macro.hxml ├── tink_priority.hxml ├── tink_pure.hxml ├── tink_slice.hxml ├── tink_streams.hxml ├── tink_stringly.hxml ├── tink_syntaxhub.hxml ├── tink_testrunner.hxml ├── tink_unittest.hxml └── travix.hxml ├── haxelib.json ├── src └── tink │ └── streams │ ├── IdealStream.hx │ ├── RealStream.hx │ ├── Stream.hx │ └── nodejs │ ├── NodejsStream.hx │ └── WrappedReadable.hx ├── tests.hxml └── tests ├── BlendTest.hx ├── NextTest.hx ├── RunTests.hx ├── SignalStreamTest.hx └── StreamTest.hx /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | test: 10 | 11 | runs-on: ubuntu-latest 12 | continue-on-error: ${{ matrix.haxe-version == 'nightly' }} 13 | 14 | strategy: 15 | matrix: 16 | haxe-version: 17 | - stable 18 | - nightly 19 | tink_core: 20 | - v1 21 | - master 22 | target: 23 | - cpp 24 | - cs -D erase-generics 25 | # - java 26 | - js 27 | - jvm 28 | # - lua 29 | - neko 30 | - node 31 | - php 32 | # - python 33 | 34 | steps: 35 | - name: Check out repo 36 | uses: actions/checkout@v3 37 | 38 | - name: Get yarn cache directory path 39 | id: yarn-cache-dir-path 40 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 41 | 42 | - name: Cache Yarn 43 | uses: actions/cache@v3 44 | with: 45 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 46 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 47 | restore-keys: | 48 | ${{ runner.os }}-yarn- 49 | 50 | - name: Cache Haxe 51 | uses: actions/cache@v3 52 | with: 53 | path: ~/haxe 54 | key: haxe 55 | 56 | - name: Install Lix 57 | uses: lix-pm/setup-lix@master 58 | 59 | - name: Install Haxe 60 | run: lix install haxe ${{ matrix.haxe-version }} 61 | 62 | - name: Install Haxe Libraries 63 | run: lix download 64 | 65 | - run: lix +tink 'core#${{ matrix.tink_core }}' 66 | 67 | - name: Run Test 68 | run: lix run travix ${{ matrix.target }} 69 | 70 | release: 71 | runs-on: ubuntu-latest 72 | needs: test 73 | if: startsWith(github.ref, 'refs/tags/') # consider using the "release" event. see: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#release 74 | 75 | steps: 76 | - name: Check out repo 77 | uses: actions/checkout@v3 78 | 79 | - name: Get yarn cache directory path 80 | id: yarn-cache-dir-path 81 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 82 | 83 | - name: Cache Yarn 84 | uses: actions/cache@v3 85 | with: 86 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 87 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 88 | restore-keys: | 89 | ${{ runner.os }}-yarn- 90 | 91 | - name: Cache Haxe 92 | uses: actions/cache@v3 93 | with: 94 | path: ~/haxe 95 | key: haxe 96 | 97 | - name: Install Lix 98 | uses: lix-pm/setup-lix@master 99 | 100 | - name: Install Haxe 101 | run: lix install haxe stable 102 | 103 | - name: Install Haxe Libraries 104 | run: lix download 105 | 106 | - name: Release to Haxelib 107 | run: lix run travix release 108 | env: 109 | HAXELIB_AUTH: ${{ secrets.HAXELIB_AUTH }} 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | node_modules 3 | *.n -------------------------------------------------------------------------------- /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.2.1", 3 | "resolveLibs": "scoped" 4 | } -------------------------------------------------------------------------------- /.travis.yml.disabled: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: xenial 3 | 4 | stages: 5 | - test 6 | - deploy 7 | 8 | language: node_js 9 | node_js: 14 10 | 11 | os: 12 | - linux 13 | # - osx 14 | 15 | env: 16 | - HAXE_VERSION=latest 17 | - HAXE_VERSION=nightly 18 | 19 | install: 20 | - npm i -g lix 21 | - lix install haxe $HAXE_VERSION 22 | - lix download 23 | 24 | script: 25 | # - lix run travix interp # runtime stack overflow 26 | - lix run travix neko 27 | - lix run travix python 28 | - lix run travix node 29 | - lix run travix js 30 | # - lix run travix flash 31 | - lix run travix java 32 | - if [[ "$(haxe -version)" =~ ^4.* ]]; then lix run travix java -D jvm; fi 33 | - lix run travix cpp 34 | # - lix run travix cs # gencs stack overflow, to be investigated 35 | - lix run travix php 36 | - lix run travix lua 37 | 38 | jobs: 39 | include: 40 | # - stage: test # should uncomment this when there is no matrix above (e.g. only one os, one env, etc) 41 | - stage: deploy 42 | language: haxe 43 | haxe: "4.2.1" 44 | os: linux 45 | install: skip 46 | script: skip 47 | env: 48 | - secure: "gPDKxw8AgAiVO8kryfw8xfQUV7RLbVypsuSBe8Cvc84+czPUK8Zfx01gj/LtEPZJ53xyA0ga50+xjzIHg+Xb/szyINmup/p4n72WUhXZ1kOaiDtSL3T/11V7dKjZUeMNS2+N9UEqE7j4ksltYRNP6Iac4dkWFTGPk960TmdZATuqfmeHs17x6flLBYjYgqzRloYIbFMYI86js9TDXfxbhedLaCaw8geYRk7NvNs3D/TUdlbPA64tgISpMi3v62uRGyvudDUXcntkTe/kjsRS8WeENCdHcAnHgEgkTArZ4qRx2eM36QN3RT7M5ZSXunpe3H1dSWErc229RYcXLQVB6sE/IaUXNW2F5KHTT35uDCzE0cR1yRPVa/E1Ois1d2aGAGqLtjkc5jpwBEZKArDTaksX1jFnDNq3oyIRb7Orcz7bgITvWWKtJ8yqOridInty8er3GMCFpGDC+5yZIpntU0nYGLL6T54zZHz6bRQF1ebnp+I4THFMRZabzbIl6QmfKbiJijXCaK9Go9l9fOaDEePwucgcH282KI19gVGshdOqTCtsyj+Q3nOPg2KMG7/7oRVjheujDrStirROjEqUH8146Srsz4RwXSjZTpzSQzuK3Ugo940SlWjedLFeDqoP0cHdjGWeSTVa+Dqyxv88+AQHGW1U2mLSYhRAca5VLX8=" 49 | after_success: 50 | - haxelib install travix 51 | - haxelib run travix install 52 | - haxelib run travix release 53 | 54 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "haxe.displayConfigurations": [ 3 | ["dev.hxml"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "args": ["run","--silent","travix","node"], 5 | "isShellCommand": true, 6 | "problemMatcher": "$haxe" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinkerbell Streams 2 | 3 | [![Build Status](https://travis-ci.org/haxetink/tink_streams.svg?branch=master)](https://travis-ci.org/haxetink/tink_streams) 4 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/haxetink/public) 5 | 6 | This library provides immutable streams, which are vaguely similar to iterators but might more accurately be thought of as immutable asynchronous lazy lists. Progressing along a stream yields a new stream instead modifying the original. The data in a stream is generated *as needed*. 7 | 8 | Because the world is a harsh place, we must discern "real" streams from "ideal" streams, where the former may yield errors, while the latter will not. To deal with this distinction, `tink_streams` makes relatively strong use of [GADTs](http://code.haxe.org/category/functional-programming/enum-gadt.html), a somewhat arcane feature of Haxe. 9 | 10 | In a nutshell, the distinction is expressed like so: 11 | 12 | ```haxe 13 | typedef RealStream = Stream; 14 | typedef IdealStream = Stream; 15 | ``` 16 | 17 | So a "real stream of items" is a "stream of items with errors", while an "ideal stream of items" is a "stream of items with nothing" (in tinkerbell `Noise` is used to express nothingness, because of the limitations `Void` exhibits). So let's have a look at what a stream looks like and how we can operate on it: 18 | 19 | ```haxe 20 | abstract Stream { 21 | function forEach(handle:Handler):Future>; 22 | } 23 | 24 | typedef Handler = Item->Future>; 25 | 26 | enum Handled { 27 | BackOff:Handled; 28 | Finish:Handled; 29 | Resume:Handled; 30 | Clog(e:Error):Handled; 31 | } 32 | 33 | enum Conclusion { 34 | Halted(rest:Stream):Conclusion; 35 | Clogged(error:Error, at:Stream):Conclusion; 36 | Failed(error:Error):Conclusion; 37 | Depleted:Conclusion; 38 | } 39 | ``` 40 | 41 | Don't let this zoo of type parameters irritate you. They all have their place and we actually know two of them already: `Item` denotes the type of items that flow through the stream and `Quality` is what we use to draw the line between ideal and real streams. 42 | 43 | There are many more functions defined on streams, but `forEach` is by far the most important one. As we see it accepts a handler for `Item` with a certain `Safety`. This is to allow us to differentiate ideal and real handlers (although we don't explicitly define them). A handler is a function that gets an item and then tells us how it has handled it. It can either `BackOff`, thus stopping iteration *before* that item, or `Finish` this stopping iteration *after* that item, it can `Resume` the iteration or - if it is a `Handler` - it may `Clog`. 44 | 45 | Iteration can be concluded for various reasons which are thus expressed in the `Conclusion` enum, which handles four cases: 46 | 47 | - `Halted`: the handler stopped the iteration. The `rest` then represent the remaining stream. So if the handler returns `BackOff` the first item of the remaining stream will be the last item the handler treated, otherwise it's all the items that would have been passed to the handler, had it not stopped. 48 | - `Clogged`: the handler raised an `error` and `at` is the remaining stream (including the item where the error was raised) 49 | - `Failed`: the stream failed. Note that no remaining stream is given, because when streams fail, they end. 50 | - `Depleted`: the whole stream was used up. Therefore this case also has no remaining stream. 51 | 52 | If the stream is ideal (and thus `Quality` is not `Error`), then `Failed` is unexpected, if the handler is ideal (and thus `Safety` is not `Error`), then `Clogged` is unexpected. 53 | -------------------------------------------------------------------------------- /dev.hxml: -------------------------------------------------------------------------------- 1 | tests.hxml 2 | -D no-deprecation-warnings 3 | -js bin/node/tests.js 4 | 5 | -lib hxnodejs 6 | -lib travix 7 | -lib tink_streams -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | # temp for development, delete this file when pure branch merged 2 | -D pure -------------------------------------------------------------------------------- /haxe_libraries/ansi.hxml: -------------------------------------------------------------------------------- 1 | -D ansi=1.0.0 2 | # @install: lix --silent download "haxelib:/ansi#1.0.0" into ansi/1.0.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/ansi/1.0.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/hxcpp.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/hxcpp#4.2.1" into hxcpp/4.2.1/haxelib 2 | # @run: haxelib run-dir hxcpp ${HAXE_LIBCACHE}/hxcpp/4.2.1/haxelib 3 | -cp ${HAXE_LIBCACHE}/hxcpp/4.2.1/haxelib/ 4 | -D hxcpp=4.2.1 -------------------------------------------------------------------------------- /haxe_libraries/hxcs.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/hxcs#4.2.0" into hxcs/4.2.0/haxelib 2 | # @run: haxelib run-dir hxcs ${HAXE_LIBCACHE}/hxcs/4.2.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/hxcs/4.2.0/haxelib/ 4 | -D hxcs=4.2.0 -------------------------------------------------------------------------------- /haxe_libraries/hxjava.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/hxjava#4.2.0" into hxjava/4.2.0/haxelib 2 | # @run: haxelib run-dir hxjava ${HAXE_LIBCACHE}/hxjava/4.2.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/hxjava/4.2.0/haxelib/ 4 | -D hxjava=4.2.0 5 | -java-lib lib/hxjava-std.jar 6 | -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/hxnodejs#12.1.0" into hxnodejs/12.1.0/haxelib 2 | -cp ${HAXE_LIBCACHE}/hxnodejs/12.1.0/haxelib/src 3 | -D hxnodejs=12.1.0 4 | --macro allowPackage('sys') 5 | # should behave like other target defines and not be defined in macro context 6 | --macro define('nodejs') 7 | --macro _internal.SuppressDeprecated.run() 8 | -------------------------------------------------------------------------------- /haxe_libraries/tink_cli.hxml: -------------------------------------------------------------------------------- 1 | -D tink_cli=0.3.1 2 | # @install: lix --silent download "haxelib:/tink_cli#0.3.1" into tink_cli/0.3.1/haxelib 3 | -lib tink_io 4 | -lib tink_stringly 5 | -lib tink_macro 6 | -cp ${HAXE_LIBCACHE}/tink_cli/0.3.1/haxelib/src 7 | # Make sure docs are generated 8 | -D use-rtti-doc -------------------------------------------------------------------------------- /haxe_libraries/tink_core.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_core#abee932c4e724517090238b6527eac28874c0354" into tink_core/1.27.1/github/abee932c4e724517090238b6527eac28874c0354 2 | -cp ${HAXE_LIBCACHE}/tink_core/1.27.1/github/abee932c4e724517090238b6527eac28874c0354/src 3 | -D tink_core=1.27.1 -------------------------------------------------------------------------------- /haxe_libraries/tink_io.hxml: -------------------------------------------------------------------------------- 1 | -D tink_io=0.5.0 2 | # @install: lix --silent download "haxelib:/tink_io#0.5.0" into tink_io/0.5.0/haxelib 3 | -lib tink_streams 4 | -lib tink_core 5 | -cp ${HAXE_LIBCACHE}/tink_io/0.5.0/haxelib/src 6 | -------------------------------------------------------------------------------- /haxe_libraries/tink_macro.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_macro#f1010a518fdefb484eaff8727ff022ba51658e53" into tink_macro/0.23.0/github/f1010a518fdefb484eaff8727ff022ba51658e53 2 | -lib tink_core 3 | -cp ${HAXE_LIBCACHE}/tink_macro/0.23.0/github/f1010a518fdefb484eaff8727ff022ba51658e53/src 4 | -D tink_macro=0.23.0 -------------------------------------------------------------------------------- /haxe_libraries/tink_priority.hxml: -------------------------------------------------------------------------------- 1 | -D tink_priority=0.1.3 2 | # @install: lix --silent download "haxelib:/tink_priority#0.1.3" into tink_priority/0.1.3/haxelib 3 | -cp ${HAXE_LIBCACHE}/tink_priority/0.1.3/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_pure.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "https://github.com/haxetink/tink_pure/archive/4050ed3a56736465484c2cab606593dca766b7bc.tar.gz" into tink_pure/0.3.2/github/4050ed3a56736465484c2cab606593dca766b7bc 2 | -D tink_pure=0.3.2 3 | -cp ${HAXESHIM_LIBCACHE}/tink_pure/0.3.2/github/4050ed3a56736465484c2cab606593dca766b7bc/src 4 | 5 | -lib tink_slice 6 | -lib tink_core -------------------------------------------------------------------------------- /haxe_libraries/tink_slice.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:tink_slice#0.1.0" into tink_slice/0.1.0/haxelib 2 | -D tink_slice=0.1.0 3 | -cp ${HAXESHIM_LIBCACHE}/tink_slice/0.1.0/haxelib/src 4 | 5 | -lib tink_core -------------------------------------------------------------------------------- /haxe_libraries/tink_streams.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -lib tink_core 3 | -D pure -------------------------------------------------------------------------------- /haxe_libraries/tink_stringly.hxml: -------------------------------------------------------------------------------- 1 | -D tink_stringly=0.2.0 2 | # @install: lix --silent download "haxelib:/tink_stringly#0.2.0" into tink_stringly/0.2.0/haxelib 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_stringly/0.2.0/haxelib/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_syntaxhub.hxml: -------------------------------------------------------------------------------- 1 | -D tink_syntaxhub=0.4.3 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_syntaxhub#8b928af11fb39170dcb7254d02923777cddcc678" into tink_syntaxhub/0.4.3/github/8b928af11fb39170dcb7254d02923777cddcc678 3 | -lib tink_priority 4 | -lib tink_macro 5 | -cp ${HAXE_LIBCACHE}/tink_syntaxhub/0.4.3/github/8b928af11fb39170dcb7254d02923777cddcc678/src 6 | --macro tink.SyntaxHub.use() -------------------------------------------------------------------------------- /haxe_libraries/tink_testrunner.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_testrunner#4b15a4d1b04eb0b37f92244cad25d2345ae41791" into tink_testrunner/0.8.0/github/4b15a4d1b04eb0b37f92244cad25d2345ae41791 2 | -lib ansi 3 | -lib tink_macro 4 | -lib tink_streams 5 | -cp ${HAXE_LIBCACHE}/tink_testrunner/0.8.0/github/4b15a4d1b04eb0b37f92244cad25d2345ae41791/src 6 | -D tink_testrunner=0.8.0 -------------------------------------------------------------------------------- /haxe_libraries/tink_unittest.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_unittest#10cfd92a3e6c42488ec981d7785835feec0fe1fd" into tink_unittest/0.7.0/github/10cfd92a3e6c42488ec981d7785835feec0fe1fd 2 | -lib tink_syntaxhub 3 | -lib tink_testrunner 4 | -cp ${HAXE_LIBCACHE}/tink_unittest/0.7.0/github/10cfd92a3e6c42488ec981d7785835feec0fe1fd/src 5 | -D tink_unittest=0.7.0 6 | --macro tink.unit.AssertionBufferInjector.use() -------------------------------------------------------------------------------- /haxe_libraries/travix.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/back2dos/travix#354c2b2a82cc3b03e2f87cc1b6f0ddc0a6a5c133" into travix/0.15.0/github/354c2b2a82cc3b03e2f87cc1b6f0ddc0a6a5c133 2 | # @post-install: cd ${HAXE_LIBCACHE}/travix/0.15.0/github/354c2b2a82cc3b03e2f87cc1b6f0ddc0a6a5c133 && haxe -cp src --run travix.PostDownload 3 | # @run: haxelib run-dir travix ${HAXE_LIBCACHE}/travix/0.15.0/github/354c2b2a82cc3b03e2f87cc1b6f0ddc0a6a5c133 4 | -lib tink_cli 5 | -cp ${HAXE_LIBCACHE}/travix/0.15.0/github/354c2b2a82cc3b03e2f87cc1b6f0ddc0a6a5c133/src 6 | -D travix=0.15.0 7 | --macro travix.Macro.setup() -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tink_streams", 3 | "license": "MIT", 4 | "tags": [ 5 | "tink", 6 | "cross", 7 | "stream", 8 | "async" 9 | ], 10 | "classPath": "src", 11 | "description": "Streams from the future. With lasers, of course ... whoaaaaa!!!!", 12 | "contributors": [ 13 | "back2dos" 14 | ], 15 | "releasenote": "Drop haxe3 support", 16 | "version": "0.4.0", 17 | "url": "http://haxetink.org/tink_streams", 18 | "dependencies": { 19 | "tink_core": "" 20 | } 21 | } -------------------------------------------------------------------------------- /src/tink/streams/IdealStream.hx: -------------------------------------------------------------------------------- 1 | package tink.streams; 2 | 3 | import tink.streams.Stream; 4 | 5 | using tink.CoreApi; 6 | 7 | @:forward @:transitive 8 | abstract IdealStream(Stream) from Stream to Stream { 9 | @:from 10 | public static inline function promiseOfIdealStream(p:Promise>):IdealStream 11 | return cast Stream.promise(p); 12 | 13 | @:from 14 | public static inline function promiseOfStreamNoise(p:Promise>):IdealStream 15 | return cast Stream.promise(p); 16 | 17 | public function collect():Future> { 18 | var buf = []; 19 | return this.forEach(function(x) { 20 | buf.push(x); 21 | return Resume; 22 | }).map(function(c) return buf); 23 | } 24 | } 25 | 26 | typedef IdealStreamObject = StreamObject; 27 | 28 | class IdealStreamBase extends StreamBase { 29 | override public function idealize(rescue:Error->Stream):IdealStream 30 | return this; 31 | } -------------------------------------------------------------------------------- /src/tink/streams/RealStream.hx: -------------------------------------------------------------------------------- 1 | package tink.streams; 2 | 3 | import tink.streams.Stream; 4 | 5 | using tink.CoreApi; 6 | 7 | @:forward @:transitive 8 | abstract RealStream(Stream) from Stream to Stream { 9 | @:from 10 | public static inline function promiseOfIdealStream(p:Promise>):RealStream 11 | return cast Stream.promise(p); 12 | 13 | @:from 14 | public static inline function promiseOfStreamNoise(p:Promise>):RealStream 15 | return cast Stream.promise(p); 16 | 17 | @:from 18 | public static inline function promiseOfRealStream(p:Promise>):RealStream 19 | return cast Stream.promise(p); 20 | 21 | @:from 22 | public static inline function promiseOfStreamError(p:Promise>):RealStream 23 | return cast Stream.promise(p); 24 | 25 | public function collect():Promise> { 26 | var buf = []; 27 | return this.forEach(function(x) { 28 | buf.push(x); 29 | return Resume; 30 | }).map(function(c) return switch c { 31 | case Depleted: Success(buf); 32 | case Failed(e): Failure(e); 33 | case Halted(_): throw 'unreachable'; 34 | }); 35 | } 36 | } 37 | typedef RealStreamObject = StreamObject; 38 | typedef RealStreamBase = StreamBase; -------------------------------------------------------------------------------- /src/tink/streams/Stream.hx: -------------------------------------------------------------------------------- 1 | package tink.streams; 2 | 3 | import tink.streams.IdealStream; 4 | 5 | using tink.CoreApi; 6 | 7 | @:forward @:transitive 8 | abstract Stream(StreamObject) from StreamObject to StreamObject { 9 | 10 | public var depleted(get, never):Bool; 11 | inline function get_depleted() 12 | return this.depleted; 13 | 14 | @:to function dirty():Stream 15 | return cast this; 16 | 17 | static public function single(i:Item):Stream 18 | return new Single(i); 19 | 20 | @:from static public function ofIterator(i:Iterator):Stream { 21 | return Generator.stream(function next(step) step(if(i.hasNext()) Link(i.next(), Generator.stream(next)) else End)); 22 | } 23 | 24 | static public function flatten(stream:Stream, Quality>):Stream { 25 | return stream.regroup(function(arr) return Converted(arr[0])); 26 | } 27 | 28 | #if cs 29 | // This is to mitigate an error in the c# generator that it generates paramterized calls 30 | // with type parameters which is not defined in scope 31 | // similar to https://github.com/HaxeFoundation/haxe/issues/6833 32 | @:from static public function dirtyFuture(f:Future>):Stream 33 | return new FutureStream(f); 34 | #end 35 | 36 | @:from static public function future(f:Future>):Stream 37 | return new FutureStream(f); 38 | 39 | #if cs 40 | // This is to mitigate an error in the c# generator that it generates paramterized calls 41 | // with type parameters which is not defined in scope 42 | // similar to https://github.com/HaxeFoundation/haxe/issues/6833 43 | @:from static public function dirtyPromise(f:Promise>):Stream 44 | return dirtyFuture(f.map(function (o) return switch o { 45 | case Success(s): s; 46 | case Failure(e): ofError(e); 47 | })); 48 | #end 49 | 50 | @:from static inline function promiseIdeal(f:Promise>):Stream 51 | return cast promise(f); 52 | 53 | @:from static inline function promiseReal(f:Promise>):Stream 54 | return cast promise(f); 55 | 56 | @:from static public function promise(f:Promise>):Stream 57 | return future(f.map(function (o) return switch o { 58 | case Success(s): s.dirty(); 59 | case Failure(e): ofError(e); 60 | })); 61 | 62 | @:from static public function ofError(e:Error):Stream 63 | return new ErrorStream(e); 64 | 65 | #if (nodejs && !macro) 66 | @:noUsing static public inline function ofNodeStream(name:String, r:js.node.stream.Readable.IReadable, ?options:{ ?onEnd:Void->Void }):RealStream { 67 | return tink.streams.nodejs.NodejsStream.wrap(name, r, options == null ? null : options.onEnd); 68 | } 69 | #end 70 | } 71 | 72 | enum RegroupStatus { 73 | Flowing:RegroupStatus; 74 | Errored(e:Error):RegroupStatus; 75 | Ended:RegroupStatus; 76 | } 77 | 78 | enum RegroupResult { 79 | Converted(data:Stream, ?untouched:Array):RegroupResult; 80 | Terminated(data:Option>):RegroupResult; 81 | Untouched:RegroupResult; 82 | Errored(e:Error):RegroupResult; 83 | } 84 | 85 | @:forward 86 | abstract Regrouper(RegrouperBase) from RegrouperBase to RegrouperBase { 87 | @:from 88 | public static function ofIgnorance(f:Array->Future>):Regrouper 89 | return {apply: function(i, _) return f(i)}; 90 | @:from 91 | public static function ofIgnoranceSync(f:Array->RegroupResult):Regrouper 92 | return {apply: function(i, _) return Future.sync(f(i))}; 93 | @:from 94 | public static function ofFunc(f:Array->RegroupStatus->Future>):Regrouper 95 | return {apply: f}; 96 | @:from 97 | public static function ofFuncSync(f:Array->RegroupStatus->RegroupResult):Regrouper 98 | return {apply: function(i, s) return Future.sync(f(i, s))}; 99 | } 100 | 101 | private typedef RegrouperBase = { 102 | function apply(input:Array, status:RegroupStatus):Future>; 103 | } 104 | 105 | private class RegroupStream extends CompoundStream { 106 | public function new(source:Stream, f:Regrouper, ?prev, ?buf) { 107 | if(prev == null) prev = Empty.make(); 108 | if(buf == null) buf = []; 109 | 110 | var ret = null; 111 | var terminated = false; 112 | var next = Stream.future(source.forEach(function(item) { 113 | buf.push(item); 114 | return f.apply(buf, Flowing).map(function (o):Handled return switch o { 115 | case Converted(v, untouched): 116 | ret = v; 117 | buf = untouched; 118 | Finish; 119 | case Terminated(v): 120 | ret = v.or(Empty.make); 121 | terminated = true; 122 | Finish; 123 | case Untouched: 124 | Resume; 125 | case Errored(e): 126 | Clog(e); 127 | }); 128 | }).map(function(o):Stream return switch o { 129 | case Failed(e): Stream.ofError(e); 130 | case Depleted if(buf.length == 0): Empty.make(); 131 | case Depleted: 132 | Stream.future(f.apply(buf, Ended).map(function(o) return switch o { 133 | case Converted(v): v; 134 | case Terminated(v): v.or(Empty.make); 135 | case Untouched: Empty.make(); 136 | case Errored(e): cast Stream.ofError(e); 137 | })); 138 | case Halted(_) if(terminated): ret; 139 | case Halted(rest): new RegroupStream(rest, f, ret, buf); 140 | case Clogged(e, _): cast new ErrorStream(e); // the regroup stream should terminate when an error occurs during the regroup process 141 | })); 142 | // TODO: get rid of those casts in this function 143 | 144 | super([prev, next]); 145 | } 146 | } 147 | 148 | enum Handled { 149 | BackOff:Handled; 150 | Finish:Handled; 151 | Resume:Handled; 152 | Clog(e:Error):Handled; 153 | } 154 | 155 | enum Conclusion { 156 | Halted(rest:Stream):Conclusion; 157 | Clogged(error:Error, at:Stream):Conclusion; 158 | Failed(error:Error):Conclusion; 159 | Depleted:Conclusion; 160 | } 161 | 162 | enum ReductionStep { 163 | Progress(result:Result):ReductionStep; 164 | Crash(e:Error):ReductionStep; 165 | } 166 | 167 | enum Reduction { 168 | Crashed(error:Error, at:Stream):Reduction; 169 | Failed(error:Error):Reduction; 170 | Reduced(result:Result):Reduction; 171 | } 172 | 173 | private class CloggedStream extends StreamBase { 174 | 175 | var rest:Stream; 176 | var error:Error; 177 | 178 | public function new(rest, error) { 179 | this.rest = rest; 180 | this.error = error; 181 | } 182 | 183 | override function next():Future> 184 | return Future.sync(Step.Fail(error)); 185 | 186 | override public function forEach(handler:Handler):Future> 187 | return Future.sync(cast Conclusion.Clogged(error, rest)); 188 | 189 | } 190 | 191 | private class ErrorStream extends StreamBase { 192 | 193 | var error:Error; 194 | 195 | public function new(error) 196 | this.error = error; 197 | 198 | override function next():Future> 199 | return Future.sync(Step.Fail(error)); 200 | 201 | override public function forEach(handler:Handler):Future> 202 | return Future.sync(Conclusion.Failed(error)); 203 | 204 | } 205 | 206 | interface StreamObject { 207 | /** 208 | * `true` if there is no data in this stream 209 | */ 210 | var depleted(get, never):Bool; 211 | function next():Future>; 212 | /** 213 | * Create a new stream by performing an N-to-M mapping 214 | */ 215 | function regroup(f:Regrouper):Stream; 216 | /** 217 | * Create a new stream by performing an 1-to-1 mapping 218 | */ 219 | function map(f:Mapping):Stream; 220 | /** 221 | * Create a filtered stream 222 | */ 223 | function filter(f:Filter):Stream; 224 | function retain():Void->Void; 225 | /** 226 | * Create an IdealStream. 227 | * The stream returned from the `rescue` function will be recursively rescued by the same `rescue` function 228 | */ 229 | function idealize(rescue:Error->Stream):IdealStream; 230 | /** 231 | * Append another stream after this 232 | */ 233 | function append(other:Stream):Stream; 234 | /** 235 | * Prepend another stream before this 236 | */ 237 | function prepend(other:Stream):Stream; 238 | function blend(other:Stream):Stream; 239 | function decompose(into:Array>):Void; 240 | /** 241 | * Iterate this stream. 242 | * The handler should return one of the following values (or a `Future` of it) 243 | * - Backoff: stop the iteration before the current item 244 | * - Finish: stop the iteration after the current item 245 | * - Resume: continue the iteration 246 | * - Clog(error): produce an error 247 | * @return A conclusion that indicates how the iteration was ended 248 | * - Depleted: there are no more data in the stream 249 | * - Failed(err): the stream produced an error 250 | * - Halted(rest): the iteration was halted by `Backoff` or `Finish` 251 | * - Clogged(err): the iteration was halted by `Clog(err)` 252 | */ 253 | function forEach(handle:Handler):Future>; 254 | /** 255 | * Think Lambda.fold() 256 | */ 257 | function reduce(initial:Result, reducer:Reducer):Future>; 258 | } 259 | 260 | class Empty extends StreamBase { 261 | 262 | function new() {} 263 | 264 | override function get_depleted() 265 | return true; 266 | 267 | override function next():Future> 268 | return Future.sync(Step.End); 269 | 270 | override public function forEach(handler:Handler):Future> 271 | return Future.sync(Depleted); 272 | 273 | static var inst = new Empty(); 274 | 275 | static public inline function make():Stream 276 | return (cast inst : Stream); 277 | 278 | } 279 | 280 | abstract Mapping(Regrouper) to Regrouper { 281 | 282 | inline function new(o) 283 | this = o; 284 | 285 | @:from static function ofNext(n:In->Promise):Mapping 286 | return new Mapping({ 287 | apply: function (i:Array, _) return n(i[0]).next(function(o) return Converted(Stream.single(o))).recover(Errored), 288 | }); 289 | 290 | @:from static function ofAsync(f:In->Future):Mapping 291 | return new Mapping({ 292 | apply: function (i:Array, _) return f(i[0]).map(function(o) return Converted(Stream.single(o))), 293 | }); 294 | 295 | @:from static function ofSync(f:In->Outcome):Mapping 296 | return new Mapping({ 297 | apply: function (i:Array, _) return Future.sync(switch f(i[0]) { 298 | case Success(v): Converted(Stream.single(v)); 299 | case Failure(e): Errored(e); 300 | }), 301 | }); 302 | 303 | @:from static function ofPlain(f:In->Out):Mapping 304 | return new Mapping({ 305 | apply: function (i:Array, _) return Future.sync(Converted(Stream.single(f(i[0])))), 306 | }); 307 | 308 | } 309 | 310 | abstract Filter(Regrouper) to Regrouper { 311 | 312 | inline function new(o) 313 | this = o; 314 | 315 | @:from static function ofNext(n:T->Promise):Filter 316 | return new Filter({ 317 | apply: function (i:Array, _) return n(i[0]).next(function (matched) return Converted(if (matched) Stream.single(i[0]) else Empty.make())).recover(Errored), 318 | }); 319 | 320 | @:from static function ofAsync(f:T->Future):Filter 321 | return new Filter({ 322 | apply: function (i:Array, _) return f(i[0]).map(function (matched) return Converted(if (matched) Stream.single(i[0]) else Empty.make())), 323 | }); 324 | 325 | @:from static function ofSync(f:T->Outcome):Filter 326 | return new Filter({ 327 | apply: function (i:Array, _) return Future.sync(switch f(i[0]) { 328 | case Success(v): Converted(if(v)Stream.single(i[0]) else Empty.make()); 329 | case Failure(e): Errored(e); 330 | }), 331 | }); 332 | 333 | @:from static function ofPlain(f:T->Bool):Filter 334 | return new Filter({ 335 | apply: function (i:Array, _) return Future.sync(Converted(if (f(i[0])) Stream.single(i[0]) else Empty.make())), 336 | }); 337 | 338 | } 339 | 340 | class StreamBase implements StreamObject { 341 | 342 | public var depleted(get, never):Bool; 343 | function get_depleted() return false; 344 | 345 | var retainCount = 0; 346 | 347 | public function retain() { 348 | retainCount++; 349 | var retained = true; 350 | return function () { 351 | if (retained) { 352 | retained = false; 353 | if (--retainCount == 0) 354 | destroy(); 355 | } 356 | } 357 | } 358 | 359 | public function next():Future> { 360 | throw 'not implemented'; 361 | // var item = null; 362 | // return this.forEach(function(i) { 363 | // item = i; 364 | // return Finish; 365 | // }).map(function(o):Step return switch o { 366 | // case Depleted: End; 367 | // case Halted(rest): Link(item, rest); 368 | // case Failed(e): Fail(e); 369 | // }); 370 | } 371 | 372 | public function regroup(f:Regrouper):Stream 373 | return new RegroupStream(this, f); 374 | 375 | public function map(f:Mapping):Stream 376 | return regroup(f); 377 | 378 | public function filter(f:Filter):Stream 379 | return regroup(f); 380 | 381 | function destroy() {} 382 | 383 | public function append(other:Stream):Stream 384 | return 385 | if (depleted) other; 386 | else CompoundStream.of([this, other]); 387 | 388 | public function prepend(other:Stream):Stream 389 | return 390 | if (depleted) other; 391 | else CompoundStream.of([other, this]); 392 | 393 | public function blend(other:Stream):Stream 394 | return 395 | if (depleted) other; 396 | else new BlendStream(this, other); 397 | 398 | public function decompose(into:Array>) 399 | if (!depleted) 400 | into.push(this); 401 | 402 | public function idealize(rescue:Error->Stream):IdealStream 403 | return 404 | if (depleted) Empty.make(); 405 | else new IdealizeStream(this, rescue); 406 | 407 | public function reduce(initial:Result, reducer:Reducer):Future> 408 | return Future.async(function (cb:Reduction->Void) { 409 | forEach(function (item) 410 | return reducer.apply(initial, item).map( 411 | function (o):Handled return switch o { 412 | case Progress(v): initial = v; Resume; 413 | case Crash(e): Clog(e); 414 | }) 415 | ).handle(function (c) switch c { 416 | case Failed(e): cb(Failed(e)); 417 | case Depleted: cb(Reduced(initial)); 418 | case Halted(_): throw "assert"; 419 | case Clogged(e, rest): cb(Crashed(e, rest)); 420 | }); 421 | }); 422 | 423 | public function forEach(handler:Handler):Future> 424 | return throw 'not implemented'; 425 | 426 | } 427 | 428 | class IdealizeStream extends IdealStreamBase { 429 | var target:Stream; 430 | var rescue:Error->Stream; 431 | 432 | public function new(target, rescue) { 433 | this.target = target; 434 | this.rescue = rescue; 435 | } 436 | 437 | override function get_depleted() 438 | return target.depleted; 439 | 440 | override function next():Future> 441 | return target.next().flatMap(function(v) return switch v { 442 | case Fail(e): rescue(e).idealize(rescue).next(); 443 | default: Future.sync(cast v); 444 | }); 445 | 446 | override public function forEach(handler:Handler):Future> 447 | return 448 | Future.async(function (cb:Conclusion->Void) 449 | target.forEach(handler).handle(function (end) switch end { 450 | case Depleted: 451 | cb(Depleted); 452 | case Halted(rest): 453 | cb(Halted(rest.idealize(rescue))); 454 | case Clogged(e, at): 455 | cb(Clogged(e, at.idealize(rescue))); 456 | case Failed(e): 457 | rescue(e).idealize(rescue).forEach(handler).handle(cb); 458 | }) 459 | ); 460 | 461 | } 462 | 463 | class Single extends StreamBase { 464 | var value:Lazy; 465 | 466 | public function new(value) 467 | this.value = value; 468 | 469 | override function next():Future> 470 | return Future.sync(Link(value.get(), Empty.make())); 471 | 472 | override public function forEach(handle:Handler) 473 | return handle.apply(value).map(function (step):Conclusion return switch step { 474 | case BackOff: 475 | Halted(this); 476 | case Finish: 477 | Halted(Empty.make()); 478 | case Resume: 479 | Depleted; 480 | case Clog(e): 481 | Clogged(e, this); 482 | }); 483 | } 484 | 485 | abstract Handler(Item->Future>) { 486 | inline function new(f) 487 | this = f; 488 | 489 | public inline function apply(item):Future> 490 | return this(item); 491 | 492 | @:from static function ofSafeSync(f:Item->Handled):Handler 493 | return new Handler(function (i) return Future.sync(f(i))); 494 | 495 | @:from static function ofUnknownSync(f:Item->Handled):Handler 496 | return new Handler(function (i) return Future.sync(f(i))); 497 | 498 | @:from static function ofSafe(f:Item->Future>):Handler 499 | return new Handler(f); 500 | 501 | @:from static function ofUnknown(f:Item->Future>):Handler 502 | return new Handler(f); 503 | } 504 | 505 | abstract Reducer(Result->Item->Future>) { 506 | inline function new(f) 507 | this = f; 508 | 509 | public inline function apply(res, item):Future> 510 | return this(res, item); 511 | 512 | @:from static function ofSafeSync(f:Result->Item->ReductionStep):Reducer 513 | return new Reducer(function (res, cur) return Future.sync(f(res, cur))); 514 | 515 | @:from static function ofUnknownSync(f:Result->Item->ReductionStep):Reducer 516 | return new Reducer(function (res, cur) return Future.sync(f(res, cur))); 517 | 518 | @:from static function ofSafe(f:Result->Item->Future>):Reducer 519 | return new Reducer(f); 520 | 521 | @:from static function ofPlainSync(f:Result->Item->Result):Reducer 522 | return new Reducer(function (res, cur) return Future.sync(Progress(f(res, cur)))); 523 | 524 | @:from static function ofUnknown(f:Result->Item->Future>):Reducer 525 | return new Reducer(f); 526 | 527 | @:from static function ofPromiseBased(f:Result->Item->Promise) 528 | return new Reducer(function (res, cur) return f(res, cur).map(function (s) return switch s { 529 | case Success(r): Progress(r); 530 | case Failure(e): Crash(e); 531 | })); 532 | 533 | } 534 | 535 | #if (java || cs) 536 | private abstract Parts(Array) { 537 | public var length(get, never):Int; 538 | inline function get_length() return this.length; 539 | 540 | public function new(parts:Array>) 541 | this = parts; 542 | 543 | @:arrayAccess function get(index:Int):Stream 544 | return this[index]; 545 | 546 | @:arrayAccess function set(index:Int, value:Stream):Stream 547 | return this[index] = value; 548 | 549 | public function copy():Parts 550 | return new Parts(cast this.copy()); 551 | 552 | public function slice(start:Int, ?end:Int):Parts 553 | return new Parts(cast this.slice(start, end)); 554 | 555 | @:from static function ofArray(a:Array>) 556 | return new Parts(a); 557 | } 558 | #else 559 | private typedef Parts = Array>; 560 | #end 561 | 562 | private class CompoundStream extends StreamBase { 563 | 564 | var parts:Parts; 565 | 566 | function new(parts) 567 | this.parts = parts; 568 | 569 | override function get_depleted() 570 | return switch parts.length { 571 | case 0: true; 572 | case 1: parts[0].depleted; 573 | default: false; 574 | } 575 | 576 | override function next():Future> { 577 | return if(parts.length == 0) Future.sync(Step.End); 578 | else parts[0].next().flatMap(function(v) return switch v { 579 | case End if(parts.length > 1): parts[1].next(); 580 | case Link(v, rest): 581 | var copy = parts.copy(); 582 | copy[0] = rest; 583 | Future.sync(Link(v, new CompoundStream(copy))); 584 | default: Future.sync(v); 585 | }); 586 | } 587 | 588 | override public function decompose(into:Array>):Void 589 | for (p in parts) 590 | p.decompose(into); 591 | 592 | override public function forEach(handler:Handler):Future> 593 | return Future.async(consumeParts.bind(cast parts, handler, _)); 594 | 595 | static function consumeParts(parts:Parts, handler:Handler, cb:Conclusion->Void) 596 | if (parts.length == 0) 597 | cb(Depleted); 598 | else 599 | (parts[0]:Stream).forEach(handler).handle(function (o) switch o { 600 | case Depleted: 601 | 602 | consumeParts(parts.slice(1), handler, cb); 603 | 604 | case Halted(rest): 605 | 606 | parts = parts.copy(); 607 | parts[0] = rest; 608 | cb(Halted(new CompoundStream(parts))); 609 | 610 | case Clogged(e, at): 611 | 612 | if (at.depleted) 613 | parts = parts.slice(1); 614 | else { 615 | parts = parts.copy(); 616 | parts[0] = at; 617 | } 618 | 619 | cb(Clogged(e, new CompoundStream(parts))); 620 | 621 | case Failed(e): 622 | 623 | cb(Failed(e)); 624 | 625 | }); 626 | 627 | static public function of(streams:Array>):Stream { 628 | 629 | var ret = []; 630 | 631 | for (s in streams) 632 | s.decompose(ret); 633 | 634 | return 635 | if (ret.length == 0) Empty.make(); 636 | else new CompoundStream(ret); 637 | } 638 | 639 | } 640 | 641 | class FutureStream extends StreamBase { 642 | var f:Future>; 643 | public function new(f) 644 | this.f = f; 645 | 646 | override function next():Future> 647 | return f.flatMap(function(s) return s.next()); 648 | 649 | override public function forEach(handler:Handler) { 650 | return Future.async(function (cb) { 651 | f.handle(function (s) s.forEach(handler).handle(cb)); 652 | }); 653 | } 654 | } 655 | 656 | class BlendStream extends Generator { 657 | 658 | public function new(a:Stream, b:Stream) { 659 | var first = null; 660 | 661 | function wait(s:Stream) { 662 | return s.next().map(function(o) { 663 | if(first == null) first = s; 664 | return o; 665 | }); 666 | } 667 | 668 | var n1 = wait(a); 669 | var n2 = wait(b); 670 | 671 | super(Future.async(function(cb) { 672 | n1.first(n2).handle(function(o) switch o { 673 | case Link(item, rest): 674 | cb(Link(item, new BlendStream(rest, first == a ? b : a))); 675 | case End: 676 | (first == a ? n2 : n1).handle(cb); 677 | case Fail(e): 678 | cb(Fail(e)); 679 | }); 680 | })); 681 | 682 | } 683 | } 684 | 685 | 686 | class Generator extends StreamBase { 687 | var upcoming:Future>; 688 | 689 | function new(upcoming) 690 | this.upcoming = upcoming; 691 | 692 | override function next():Future> 693 | return upcoming; 694 | 695 | override public function forEach(handler:Handler) 696 | return Future.async(function (cb:Conclusion->Void) 697 | upcoming.handle(function (e) switch e { 698 | case Link(v, then): 699 | handler.apply(v).handle(function (s) switch s { 700 | case BackOff: 701 | cb(Halted(this)); 702 | case Finish: 703 | cb(Halted(then)); 704 | case Resume: 705 | then.forEach(handler).handle(cb); 706 | case Clog(e): 707 | cb(Clogged(e, this)); 708 | }); 709 | case Fail(e): 710 | cb(Failed(e)); 711 | case End: 712 | cb(Depleted); 713 | }) 714 | ); 715 | 716 | static public function stream(step:(Step->Void)->Void) { 717 | return new Generator(Future.async(step)); 718 | } 719 | 720 | } 721 | 722 | enum Step { 723 | Link(value:Item, next:Stream):Step; 724 | Fail(e:Error):Step; 725 | End:Step; 726 | } 727 | 728 | class SignalStream extends Generator { 729 | public function new(signal:Signal>) 730 | super( 731 | signal.nextTime().map(function(o):Step return switch o { 732 | case Data(data): Link(data, new SignalStream(signal)); 733 | case Fail(e): Fail(e); 734 | case End: End; 735 | }).eager() // this must be eager, otherwise the signal will "run away" if there's no consumer for this stream 736 | ); 737 | } 738 | 739 | enum Yield { 740 | Data(data:Item):Yield; 741 | Fail(e:Error):Yield; 742 | End:Yield; 743 | } 744 | -------------------------------------------------------------------------------- /src/tink/streams/nodejs/NodejsStream.hx: -------------------------------------------------------------------------------- 1 | package tink.streams.nodejs; 2 | 3 | import tink.streams.Stream; 4 | 5 | using tink.CoreApi; 6 | 7 | class NodejsStream extends Generator { 8 | 9 | function new(target:WrappedReadable) { 10 | super(Future.async(function (cb) { 11 | target.read().handle(function (o) cb(switch o { 12 | case Success(null): End; 13 | case Success(data): Link(data, new NodejsStream(target)); 14 | case Failure(e): Fail(e); 15 | })); 16 | })); 17 | } 18 | static public function wrap(name, native, onEnd) 19 | return new NodejsStream(new WrappedReadable(name, native, onEnd)); 20 | 21 | } -------------------------------------------------------------------------------- /src/tink/streams/nodejs/WrappedReadable.hx: -------------------------------------------------------------------------------- 1 | package tink.streams.nodejs; 2 | 3 | import js.node.Buffer; 4 | import js.node.stream.Readable.IReadable; 5 | 6 | using tink.CoreApi; 7 | 8 | class WrappedReadable { 9 | 10 | var native:IReadable; 11 | var name:String; 12 | var end:Surprise, Error>; 13 | 14 | public function new(name, native, onEnd) { 15 | this.name = name; 16 | this.native = native; 17 | 18 | end = Future.async(function (cb) { 19 | native.once('end', function () cb(Success(null))); 20 | native.once('close', function () cb(Success(null))); 21 | native.once('error', function (e:{ code:String, message:String }) cb(Failure(new Error('${e.code} - Failed reading from $name because ${e.message}')))); 22 | }) 23 | .eager(); // async laziness fix for tink_core v2 24 | if (onEnd != null) 25 | end.handle(function () 26 | js.Node.process.nextTick(onEnd) 27 | ); 28 | } 29 | 30 | public function read():Promise>{ 31 | return Future.async(function (cb) { 32 | function attempt() { 33 | try 34 | switch native.read() { 35 | case null: 36 | native.once('readable', attempt); 37 | case object: 38 | cb(Success((cast object:T))); 39 | } 40 | catch (e:Dynamic) { 41 | cb(Failure(Error.withData('Error while reading from $name', e))); 42 | } 43 | } 44 | 45 | attempt(); 46 | //end.handle(cb); 47 | }).first(end); 48 | } 49 | } -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | -cp ./tests/ 2 | -main RunTests 3 | -lib tink_unittest -------------------------------------------------------------------------------- /tests/BlendTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.streams.Stream; 4 | using StringTools; 5 | 6 | using tink.CoreApi; 7 | 8 | @:asserts 9 | class BlendTest { 10 | public function new() {} 11 | 12 | public function testBlend() { 13 | var done = false; 14 | var a = Signal.trigger(); 15 | var b = Signal.trigger(); 16 | var blended = new SignalStream(a.asSignal()).blend(new SignalStream(b.asSignal())); 17 | a.trigger(Data(1)); 18 | b.trigger(Data(2)); 19 | a.trigger(Data(3)); 20 | 21 | var i = 0; 22 | var sum = 0; 23 | var result = blended.forEach(function (v) { 24 | asserts.assert(++i == v); 25 | sum += v; 26 | return Resume; 27 | }); 28 | 29 | a.trigger(Data(4)); 30 | a.trigger(End); 31 | b.trigger(Data(5)); 32 | b.trigger(End); 33 | b.trigger(Data(6)); 34 | a.trigger(Data(7)); 35 | 36 | result.handle(function (x) { 37 | asserts.assert(Depleted == x); 38 | asserts.assert(15 == sum); 39 | done = true; 40 | }); 41 | asserts.assert(done); 42 | return asserts.done(); 43 | } 44 | 45 | public function testCompound() { 46 | var done = false; 47 | var a = Signal.trigger(); 48 | var b = Signal.trigger(); 49 | var c = Signal.trigger(); 50 | var blended = new SignalStream(a).append(new SignalStream(c)).blend(new SignalStream(b)); 51 | a.trigger(Data(1)); 52 | b.trigger(Data(2)); 53 | a.trigger(End); 54 | c.trigger(Data(3)); 55 | 56 | var i = 0; 57 | var sum = 0; 58 | var result = blended.forEach(function (v) { 59 | asserts.assert(++i == v); 60 | sum += v; 61 | return Resume; 62 | }); 63 | 64 | c.trigger(Data(4)); 65 | c.trigger(End); 66 | b.trigger(Data(5)); 67 | b.trigger(End); 68 | b.trigger(Data(6)); 69 | a.trigger(Data(7)); 70 | 71 | result.handle(function (x) { 72 | asserts.assert(Depleted == x); 73 | asserts.assert(15 == sum); 74 | done = true; 75 | }); 76 | asserts.assert(done); 77 | return asserts.done(); 78 | } 79 | 80 | public function testError() { 81 | var done = false; 82 | var a = Signal.trigger(); 83 | var b = Signal.trigger(); 84 | var blended = new SignalStream(a.asSignal()).blend(new SignalStream(b.asSignal())); 85 | a.trigger(Data(1)); 86 | b.trigger(Data(2)); 87 | a.trigger(Data(3)); 88 | 89 | var i = 0; 90 | var sum = 0; 91 | var result = blended.forEach(function (v) { 92 | asserts.assert(++i == v); 93 | sum += v; 94 | return Resume; 95 | }); 96 | 97 | a.trigger(Data(4)); 98 | a.trigger(Data(5)); 99 | b.trigger(Fail(new Error('Failed'))); 100 | a.trigger(End); 101 | 102 | result.handle(function (x) { 103 | asserts.assert(x.match(Failed(_))); 104 | asserts.assert(15 == sum); 105 | done = true; 106 | }); 107 | asserts.assert(done); 108 | return asserts.done(); 109 | } 110 | 111 | public function testReuse() { 112 | var a = Signal.trigger(); 113 | var b = Signal.trigger(); 114 | var blended = new SignalStream(a.asSignal()).blend(new SignalStream(b.asSignal())); 115 | a.trigger(Data(1)); 116 | b.trigger(Data(2)); 117 | b.trigger(End); 118 | a.trigger(Data(3)); 119 | a.trigger(End); 120 | 121 | var count = 0; 122 | function iterate() { 123 | var i = 0; 124 | var sum = 0; 125 | blended.forEach(function (v) { 126 | asserts.assert(++i == v); 127 | sum += v; 128 | return Resume; 129 | }).handle(function (x) { 130 | asserts.assert(Depleted == x); 131 | asserts.assert(6 == sum); 132 | count++; 133 | }); 134 | } 135 | 136 | iterate(); 137 | iterate(); 138 | iterate(); 139 | asserts.assert(3 == count); 140 | return asserts.done(); 141 | } 142 | } -------------------------------------------------------------------------------- /tests/NextTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.unit.*; 4 | import tink.streams.Stream; 5 | using StringTools; 6 | 7 | using tink.CoreApi; 8 | 9 | @:asserts 10 | class NextTest { 11 | public function new() {} 12 | public function testMap() { 13 | var a = Stream.ofIterator(0...3); 14 | var b = a.map(function(v) return v + 1); 15 | check(asserts, b, [1,2,3]); 16 | return asserts.done(); 17 | } 18 | 19 | public function testFilter() { 20 | var a = Stream.ofIterator(0...6); 21 | var b = a.filter(function(v) return v % 2 == 1); 22 | check(asserts, b, [1,3,5]); 23 | return asserts.done(); 24 | } 25 | 26 | public function testCompound() { 27 | var a = Signal.trigger(); 28 | var b = Signal.trigger(); 29 | var compound = new SignalStream(a).append(new SignalStream(b)); 30 | a.trigger(Data(1)); 31 | b.trigger(Data(3)); 32 | a.trigger(Data(2)); 33 | a.trigger(End); 34 | check(asserts, compound, [1,2,3]); 35 | 36 | var a = Stream.ofIterator(0...3); 37 | var b = Stream.ofIterator(0...3); 38 | var compound = a.append(b); 39 | check(asserts, compound, [0,1,2,0,1,2]); 40 | return asserts.done(); 41 | } 42 | 43 | public function testBlend() { 44 | var a = Signal.trigger(); 45 | var b = Signal.trigger(); 46 | var compound = new SignalStream(a).blend(new SignalStream(b)); 47 | a.trigger(Data(1)); 48 | b.trigger(Data(2)); 49 | a.trigger(Data(3)); 50 | a.trigger(End); 51 | b.trigger(End); 52 | check(asserts, compound, [1,2,3]); 53 | return asserts.done(); 54 | } 55 | 56 | function check(asserts:AssertionBuffer, stream:Stream, values:Array, ?pos:haxe.PosInfos) { 57 | var current = stream; 58 | for(i in 0...values.length) { 59 | current.next().handle(function(v) switch v { 60 | case Link(v, rest): asserts.assert(values[i] == v, pos); current = rest; 61 | default: asserts.fail('Expected Link(_)', pos); 62 | }); 63 | } 64 | current.next().handle(function(v) asserts.assert(v.match(End), pos)); 65 | } 66 | } -------------------------------------------------------------------------------- /tests/RunTests.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.unit.*; 4 | import tink.testrunner.*; 5 | 6 | using tink.CoreApi; 7 | 8 | class RunTests { 9 | 10 | static function main() { 11 | 12 | #if python 13 | (cast python.lib.Sys).setrecursionlimit(9999); 14 | #end 15 | 16 | Runner.run(TestBatch.make([ 17 | new StreamTest(), 18 | new BlendTest(), 19 | new NextTest(), 20 | new SignalStreamTest(), 21 | ])).handle(Runner.exit); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/SignalStreamTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.streams.Stream; 4 | using StringTools; 5 | 6 | using tink.CoreApi; 7 | 8 | @:asserts 9 | class SignalStreamTest { 10 | public function new() {} 11 | public function testNormal() { 12 | var done = false; 13 | var a = Signal.trigger(); 14 | var stream = new SignalStream(a.asSignal()); 15 | a.trigger(Data(1)); 16 | a.trigger(Data(2)); 17 | a.trigger(Data(3)); 18 | 19 | var i = 0; 20 | var sum = 0; 21 | var result = stream.forEach(function (v) { 22 | asserts.assert(++i == v); 23 | sum += v; 24 | return Resume; 25 | }); 26 | 27 | a.trigger(Data(4)); 28 | a.trigger(Data(5)); 29 | a.trigger(End); 30 | a.trigger(Data(6)); 31 | a.trigger(Data(7)); 32 | 33 | result.handle(function (x) { 34 | asserts.assert(Depleted == x); 35 | asserts.assert(15 == sum); 36 | done = true; 37 | }); 38 | asserts.assert(done); 39 | return asserts.done(); 40 | } 41 | 42 | public function testError() { 43 | var done = false; 44 | var a = Signal.trigger(); 45 | var stream = new SignalStream(a.asSignal()); 46 | a.trigger(Data(1)); 47 | a.trigger(Data(2)); 48 | a.trigger(Data(3)); 49 | 50 | var i = 0; 51 | var sum = 0; 52 | var result = stream.forEach(function (v) { 53 | asserts.assert(++i == v); 54 | sum += v; 55 | return Resume; 56 | }); 57 | 58 | a.trigger(Data(4)); 59 | a.trigger(Data(5)); 60 | a.trigger(Fail(new Error('Failed'))); 61 | 62 | result.handle(function (x) { 63 | asserts.assert(x.match(Failed(_))); 64 | asserts.assert(15 == sum); 65 | done = true; 66 | }); 67 | asserts.assert(done); 68 | return asserts.done(); 69 | } 70 | 71 | public function testReuse() { 72 | var a = Signal.trigger(); 73 | var stream = new SignalStream(a.asSignal()); 74 | a.trigger(Data(1)); 75 | a.trigger(Data(2)); 76 | a.trigger(Data(3)); 77 | a.trigger(End); 78 | 79 | var count = 0; 80 | function iterate() { 81 | var i = 0; 82 | var sum = 0; 83 | stream.forEach(function (v) { 84 | asserts.assert(++i == v); 85 | sum += v; 86 | return Resume; 87 | }).handle(function (x) { 88 | asserts.assert(Depleted == x); 89 | asserts.assert(6 == sum); 90 | count++; 91 | }); 92 | } 93 | 94 | iterate(); 95 | iterate(); 96 | iterate(); 97 | asserts.assert(3 == count); 98 | return asserts.done(); 99 | } 100 | } -------------------------------------------------------------------------------- /tests/StreamTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.streams.IdealStream; 4 | import tink.streams.RealStream; 5 | import tink.streams.Stream; 6 | using StringTools; 7 | 8 | using tink.CoreApi; 9 | 10 | @:asserts 11 | @:timeout(200000) 12 | class StreamTest { 13 | public function new() {} 14 | public function testIterator() { 15 | var s = Stream.ofIterator(0...100); 16 | var sum = 0; 17 | s.forEach(function (v) { 18 | sum += v; 19 | return Resume; 20 | }).handle(function (x) { 21 | asserts.assert(Depleted == x); 22 | asserts.assert(4950 == sum); 23 | asserts.done(); 24 | }); 25 | return asserts; 26 | } 27 | 28 | public function testMapFilter() { 29 | 30 | var s = Stream.ofIterator(0...100); 31 | 32 | s = s.filter(function (i) return i % 2 == 0); 33 | s = s.filter(function (i) return Success(i % 3 == 0)); 34 | s = s.map(function (i) return i * 3); 35 | s = s.filter(function (i) return Future.sync(i % 5 == 0)); 36 | s = s.filter(function (i) return Promise.lift(i > 100)); 37 | s = s.map(function (i) return Success(i << 1)); 38 | s = s.map(function (i) return Promise.lift(i + 13)); 39 | s = s.map(function (i) return Future.sync(i - 3)); 40 | s = s.map(function (i) return i * 2); 41 | 42 | var sum = 0; 43 | 44 | s.forEach(function (v) return Future.sync(Resume)).handle(function (c) switch c { 45 | case Failed(_): 46 | default: 47 | }); 48 | 49 | s.idealize(null).forEach(function (v) { 50 | sum += v; 51 | return Future.sync(Resume); 52 | }).handle(function (x) switch x { 53 | case Depleted: 54 | asserts.assert(1840 == sum); 55 | asserts.done(); 56 | case Halted(_): 57 | asserts.fail('Expected "Depleted'); 58 | }); 59 | return asserts; 60 | } 61 | 62 | public function testMapError() { 63 | var s = Stream.ofIterator(0...100); 64 | var mapped = s.map(function(v) return v % 5 == 4 ? Failure(new Error('Fail $v')) : Success(v)); 65 | var sum = 0; 66 | 67 | mapped.forEach(function(v) { 68 | sum += v; 69 | return Resume; 70 | }).handle(function(o) switch o { 71 | case Depleted: 72 | asserts.fail('Expected "Failed'); 73 | case Failed(e): 74 | asserts.assert(e.message == 'Fail 4'); 75 | asserts.assert(sum == 6); 76 | asserts.done(); 77 | case Halted(_): 78 | asserts.fail('Expected "Failed'); 79 | }); 80 | 81 | return asserts; 82 | } 83 | 84 | public function testRegroup() { 85 | 86 | var s = Stream.ofIterator(0...100); 87 | 88 | var sum = 0; 89 | s.regroup(function (i:Array) return i.length == 5 ? Converted(Stream.single(i[0] + i[4])) : Untouched) 90 | .idealize(null).forEach(function (v) { 91 | sum += v; 92 | return Resume; 93 | }) 94 | .handle(function (x) switch x { 95 | case Depleted: 96 | asserts.assert(1980 == sum); 97 | case Halted(_): 98 | asserts.fail('Expected "Depleted"'); 99 | }); 100 | 101 | var sum = 0; 102 | s.regroup(function (i:Array, s) { 103 | return if(s == Flowing) 104 | i.length == 3 ? Converted(Stream.single(i[0] + i[2])) : Untouched 105 | else 106 | Converted(Stream.single(i[0])); // TODO: test backoff / clog at last step 107 | }) 108 | .idealize(null).forEach(function (v) { 109 | sum += v; 110 | return Resume; 111 | }) 112 | .handle(function (x) switch x { 113 | case Depleted: 114 | asserts.assert(3333 == sum); 115 | case Halted(_): 116 | asserts.fail('Expected "Depleted"'); 117 | }); 118 | 119 | var sum = 0; 120 | s.regroup(function (i:Array) return Converted([i[0], i[0]].iterator())) 121 | .idealize(null).forEach(function (v) { 122 | sum += v; 123 | return Resume; 124 | }) 125 | .handle(function (x) switch x { 126 | case Depleted: 127 | asserts.assert(9900 == sum); 128 | case Halted(_): 129 | asserts.fail('Expected "Depleted"'); 130 | }); 131 | 132 | var sum = 0; 133 | s.regroup(function (i:Array, status:RegroupStatus) { 134 | var batch = null; 135 | 136 | if(status == Ended) 137 | batch = i; 138 | else if(i.length > 3) 139 | batch = i.splice(0, 3); // leave one item in the buf 140 | 141 | return if(batch != null) 142 | Converted(batch.iterator(), i) 143 | else 144 | Untouched; 145 | }) 146 | .idealize(null).forEach(function (v) { 147 | sum += v; 148 | return Resume; 149 | }) 150 | .handle(function (x) switch x { 151 | case Depleted: 152 | asserts.assert(4950 == sum); 153 | case Halted(_): 154 | asserts.fail('Expected "Depleted"'); 155 | }); 156 | 157 | return asserts.done(); 158 | } 159 | 160 | public function testNested() { 161 | var n = Stream.ofIterator([Stream.ofIterator(0...3), Stream.ofIterator(3...6)].iterator()); 162 | var s = Stream.flatten(n); 163 | var sum = 0; 164 | 165 | s.forEach(function (v) { 166 | sum += v; 167 | return Resume; 168 | }).handle(function (x) { 169 | asserts.assert(Depleted == x); 170 | asserts.assert(15 == sum); 171 | asserts.done(); 172 | }); 173 | 174 | return asserts; 175 | } 176 | 177 | public function testNestedWithInnerError() { 178 | var n = Stream.ofIterator([ 179 | Stream.ofIterator(0...3), 180 | ofOutcomes([Success(3), Failure(new Error('dummy')), Success(5)].iterator()), 181 | Stream.ofIterator(6...9), 182 | ].iterator()); 183 | var s = Stream.flatten(n); 184 | var sum = 0; 185 | 186 | s.forEach(function (v) { 187 | sum += v; 188 | return Resume; 189 | }).handle(function (x) { 190 | asserts.assert(x.match(Failed(_))); 191 | asserts.assert(6 == sum); 192 | asserts.done(); 193 | }); 194 | 195 | return asserts; 196 | } 197 | 198 | public function testNestedWithOuterError() { 199 | var n = ofOutcomes([ 200 | Success(Stream.ofIterator(0...3)), 201 | Failure(new Error('dummy')), 202 | Success(Stream.ofIterator(6...9)), 203 | ].iterator()); 204 | 205 | var s = Stream.flatten(n); 206 | var sum = 0; 207 | 208 | s.forEach(function (v) { 209 | sum += v; 210 | return Resume; 211 | }).handle(function (x) { 212 | asserts.assert(x.match(Failed(_))); 213 | asserts.assert(3 == sum); 214 | asserts.done(); 215 | }); 216 | 217 | return asserts; 218 | } 219 | 220 | #if !java 221 | public function casts() { 222 | var pi1:Promise> = Promise.reject(new Error('dummy')); 223 | var pi2:Promise> = Promise.reject(new Error('dummy')); 224 | var pr1:Promise> = Promise.reject(new Error('dummy')); 225 | var pr2:Promise> = Promise.reject(new Error('dummy')); 226 | var r1:RealStream; 227 | var r2:Stream; 228 | 229 | r1 = pi1; 230 | r2 = pi1; 231 | r1 = pi2; 232 | r2 = pi2; 233 | 234 | r1 = pr1; 235 | r2 = pr1; 236 | r1 = pr2; 237 | r2 = pr2; 238 | 239 | return asserts.done(); 240 | } 241 | #end 242 | 243 | 244 | // maybe useful to be moved to Stream itself 245 | inline function ofOutcomes(i:Iterator>) { 246 | return Stream.ofIterator(i).map(function(v:Outcome) return v); 247 | } 248 | } --------------------------------------------------------------------------------