cordis 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 390ce9e78dbb7edfacb5cd004d46448eadd6c6ccffad5eaee9ef190c2b9a15a2
4
+ data.tar.gz: af955c8b180e217685481a3da9d27d9ed56bb95ce1b74c49f7f42296f1469876
5
+ SHA512:
6
+ metadata.gz: 25e46a5eff3a9e977fbcaf1ccddf72ae83a3b0980d9f00aa24afac3a188fb2a284eb822955d15c64bea68b547972a552d2267b6a8c92560d28a0d72d57cc47a4
7
+ data.tar.gz: 95e3fa579b1a1c743aac12a4c7649b45e27df8f1882b0cb3c6ce9f7a96cd55aa052732ca12baeea66f559c02c9de8fbb0667cfd566a19fa5baad8a236505ffd8
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # cordis-rb
2
+
3
+ [English](#english) | [繁體中文](#繁體中文) | [日本語](#日本語)
4
+
5
+ ---
6
+
7
+ ## English
8
+
9
+ > ⚠️ **Experimental** — a learning/research project, not production-ready. APIs change without notice.
10
+
11
+ cordis-rb is a Ruby reimplementation of the core mechanisms of [Cordis](https://github.com/cordiverse/cordis) (a TypeScript plugin/lifecycle meta-framework), based on the paper *"A Programming Paradigm for Spatiotemporal Composability"*. It is built on the [async](https://github.com/socketry/async) gem, using Fiber-based cooperative concurrency in place of the event loop that JavaScript gets for free.
12
+
13
+ ### What's here so far
14
+
15
+ Aligned with upstream `packages/core` (4.0 naming: `Fiber`, formerly `EffectScope`):
16
+
17
+ - **Revertible effects** — `ctx.effect` applies a side effect and registers its inverse (single or multi-step); disposal is strict LIFO (Theorem 16)
18
+ - **Plugin system** — `ctx.plugin` applies a plugin *as a revertible effect* on the parent fiber, so the whole plugin tree is one nested effect tree; loading is deferred one tick, load/unload transitions are serialized per fiber (inertia lock)
19
+ - **Reactive coeffects** — `ctx.provide` / `ctx.inject`: consumers load when their dependencies are satisfied, reload when a provider is swapped, and are torn down *before* their provider finishes unloading
20
+ - **Events** — `ctx.on` / `once` / `emit` / `bail` / `waterfall` (sync) and `ctx.parallel` / `serial` (async); listeners are effects, removed automatically on fiber disposal
21
+ - **Isolation & intercept** — `ctx.isolate` puts a service name in its own realm (provides/injects no longer cross the boundary; share a realm by passing the same label); `ctx.intercept` / Hash inject configs carry per-caller config, resolved with `ctx.resolve_config`
22
+ - **Service base class** — subclass `Cordis::Service`, declare `provide :name` (+ optional `inject`), and pass the class to `ctx.plugin`: the instance provides itself, and `#init` runs as the load body (blocking there keeps the service invisible to dependents — pending inject)
23
+
24
+ ```ruby
25
+ ctx = Cordis::Context.new
26
+
27
+ Sync do
28
+ provider = ctx.plugin(lambda { |c, _config|
29
+ db = Database.connect
30
+ c.effect { -> { db.close } } # registered before provide → runs after all dependents are gone
31
+ c.provide(:db, db)
32
+ })
33
+
34
+ ctx.inject([:db]) do |c, _config|
35
+ c.on('request') { |req| c.db.query(req) } # loads only once :db is active
36
+ end
37
+
38
+ provider.await # wait for the deferred load
39
+ provider.dispose # dependents tear down first, then the provider, LIFO
40
+ end
41
+ ```
42
+
43
+ ### Roadmap
44
+
45
+ - [x] Revertible effects with LIFO disposal
46
+ - [x] Plugin/fiber lifecycle (epoch + inertia state machine, via `async`)
47
+ - [x] Reactive coeffects (`ctx.provide` / `ctx.inject`)
48
+ - [x] Event system (`ctx.on`, waterfall included)
49
+ - [x] Isolation & intercept (`ctx.isolate` / `ctx.intercept`)
50
+ - [x] `Cordis::Service` base class
51
+ - [ ] Loader / hot-reload reconciliation — maybe, later
52
+
53
+ ### Installation
54
+
55
+ ```ruby
56
+ gem 'cordis', github: '5xRuby/cordis-rb' # not yet published to rubygems.org
57
+ ```
58
+
59
+ ### Development
60
+
61
+ ```
62
+ bundle install && bundle exec rspec
63
+ bundle exec ruby examples/demo.rb # lifecycle / coeffect walkthrough
64
+ ruby examples/webapp.rb # the same story with Sinatra + Falcon + SQLite
65
+ ```
66
+
67
+ ### License
68
+
69
+ [Apache-2.0](LICENSE)
70
+
71
+ ---
72
+
73
+ ## 繁體中文
74
+
75
+ > ⚠️ **實驗性專案** — 練習/研究用途,不求生產可用,API 隨時會變。
76
+
77
+ cordis-rb 用 Ruby 重新實作 [Cordis](https://github.com/cordiverse/cordis)(TypeScript 的 plugin/生命週期 meta-framework)的核心機制,理論基礎來自論文 *"A Programming Paradigm for Spatiotemporal Composability"*。以 [async](https://github.com/socketry/async) gem 為併發基底 —— JavaScript 有 event loop 免費提供協作式併發,Ruby 沒有對應物,這裡用 Fiber-based 的 async 補上這塊。
78
+
79
+ ### 目前完成
80
+
81
+ 對接上游 `packages/core`(4.0 命名:`Fiber`,即原本的 `EffectScope`):
82
+
83
+ - **Revertible effect** — `ctx.effect` 套用副作用並登記 inverse(單步或多步);撤除嚴格 LIFO(Theorem 16)
84
+ - **Plugin 系統** — `ctx.plugin` 把 plugin *當成一個 revertible effect* 掛在 parent fiber 上,整棵 plugin tree 就是巢狀 effect tree;載入延後一個 tick,每個 fiber 的 load/unload 由 inertia lock 序列化
85
+ - **Reactive coeffect** — `ctx.provide` / `ctx.inject`:依賴滿足才載入、provider 被換掉就 reload、provider 卸載前依賴者先 teardown
86
+ - **事件系統** — `ctx.on` / `once` / `emit` / `bail` / `waterfall`(同步)與 `ctx.parallel` / `serial`(非同步);listener 就是 effect,fiber 卸載時自動移除
87
+ - **Isolation 與 intercept** — `ctx.isolate` 讓某個 service name 進入獨立 realm(provide/inject 不再跨界;傳同一個 label 可共用 realm);`ctx.intercept` 與 Hash 形式的 inject config 攜帶 per-caller 設定,用 `ctx.resolve_config` 解析
88
+ - **Service base class** — 繼承 `Cordis::Service`、宣告 `provide :name`(可加 `inject`),把 class 直接丟給 `ctx.plugin`:instance 會 provide 自己,`#init` 就是載入本體(在裡面 block 住,service 對依賴者就不可見 —— pending inject)
89
+
90
+ ```ruby
91
+ ctx = Cordis::Context.new
92
+
93
+ Sync do
94
+ provider = ctx.plugin(lambda { |c, _config|
95
+ db = Database.connect
96
+ c.effect { -> { db.close } } # 註冊在 provide 之前 → 所有依賴者卸載後才執行
97
+ c.provide(:db, db)
98
+ })
99
+
100
+ ctx.inject([:db]) do |c, _config|
101
+ c.on('request') { |req| c.db.query(req) } # :db active 之後才載入
102
+ end
103
+
104
+ provider.await # 等延後的載入完成
105
+ provider.dispose # 依賴者先卸載,再撤 provider,LIFO
106
+ end
107
+ ```
108
+
109
+ ### Roadmap
110
+
111
+ - [x] Revertible effect 與 LIFO 撤除
112
+ - [x] Plugin/fiber 生命週期(epoch + inertia 狀態機,基於 `async`)
113
+ - [x] Reactive coeffect(`ctx.provide` / `ctx.inject`)
114
+ - [x] 事件系統(`ctx.on`,含 waterfall)
115
+ - [x] Isolation 與 intercept(`ctx.isolate` / `ctx.intercept`)
116
+ - [x] `Cordis::Service` base class
117
+ - [ ] Loader / hot-reload reconciliation — 骨架穩了再說
118
+
119
+ ### 安裝
120
+
121
+ ```ruby
122
+ gem 'cordis', github: '5xRuby/cordis-rb' # 尚未發佈到 rubygems.org
123
+ ```
124
+
125
+ ### 開發
126
+
127
+ ```
128
+ bundle install && bundle exec rspec
129
+ bundle exec ruby examples/demo.rb # 生命週期 / coeffect 導覽
130
+ ruby examples/webapp.rb # 同一個故事,換成 Sinatra + Falcon + SQLite
131
+ ```
132
+
133
+ ### 授權
134
+
135
+ [Apache-2.0](LICENSE)
136
+
137
+ ---
138
+
139
+ ## 日本語
140
+
141
+ > ⚠️ **実験的プロジェクト** — 学習・研究目的であり、プロダクション利用は想定していません。API は予告なく変わります。
142
+
143
+ cordis-rb は、[Cordis](https://github.com/cordiverse/cordis)(TypeScript 製の plugin / ライフサイクル meta-framework)のコア機構を Ruby で再実装するプロジェクトです。理論的基盤は論文 *"A Programming Paradigm for Spatiotemporal Composability"* にあります。並行処理の基盤には [async](https://github.com/socketry/async) gem を採用 —— JavaScript ではイベントループが協調的並行性を無償で提供しますが、Ruby には相当物がないため、Fiber ベースの async でその役割を担います。
144
+
145
+ ### 現状
146
+
147
+ 上流 `packages/core` に整合(4.0 命名:`Fiber`、旧 `EffectScope`):
148
+
149
+ - **Revertible effect** — `ctx.effect` が副作用を適用して inverse を登録(単段・多段);巻き戻しは厳密な LIFO(Theorem 16)
150
+ - **Plugin システム** — `ctx.plugin` は plugin を *revertible effect として* parent fiber に掛けるため、plugin ツリー全体が入れ子の effect ツリーになる;ロードは 1 tick 遅延、fiber ごとの load/unload は inertia lock で直列化
151
+ - **Reactive coeffect** — `ctx.provide` / `ctx.inject`:依存が満たされたらロード、provider が入れ替われば reload、provider のアンロード前に依存側が先に teardown
152
+ - **イベントシステム** — `ctx.on` / `once` / `emit` / `bail` / `waterfall`(同期)と `ctx.parallel` / `serial`(非同期);listener は effect であり、fiber の破棄時に自動で外れる
153
+ - **Isolation と intercept** — `ctx.isolate` は service name を独立した realm に隔離(provide/inject は境界を越えない;同じ label を渡せば realm を共有);`ctx.intercept` と Hash 形式の inject config は per-caller 設定を運び、`ctx.resolve_config` で解決
154
+ - **Service base class** — `Cordis::Service` を継承し `provide :name`(必要なら `inject` も)を宣言、class をそのまま `ctx.plugin` に渡す:instance が自身を provide し、`#init` がロード本体になる(そこで block すれば service は依存側から不可視のまま —— pending inject)
155
+
156
+ ```ruby
157
+ ctx = Cordis::Context.new
158
+
159
+ Sync do
160
+ provider = ctx.plugin(lambda { |c, _config|
161
+ db = Database.connect
162
+ c.effect { -> { db.close } } # provide より先に登録 → 依存側が全て消えた後に実行
163
+ c.provide(:db, db)
164
+ })
165
+
166
+ ctx.inject([:db]) do |c, _config|
167
+ c.on('request') { |req| c.db.query(req) } # :db が active になってからロード
168
+ end
169
+
170
+ provider.await # 遅延ロードの完了を待つ
171
+ provider.dispose # 依存側が先に teardown、その後 provider、LIFO
172
+ end
173
+ ```
174
+
175
+ ### Roadmap
176
+
177
+ - [x] Revertible effect と LIFO 巻き戻し
178
+ - [x] Plugin/fiber ライフサイクル(epoch + inertia ステートマシン、`async` ベース)
179
+ - [x] Reactive coeffect(`ctx.provide` / `ctx.inject`)
180
+ - [x] イベントシステム(`ctx.on`、waterfall 含む)
181
+ - [x] Isolation と intercept(`ctx.isolate` / `ctx.intercept`)
182
+ - [x] `Cordis::Service` base class
183
+ - [ ] Loader / hot-reload reconciliation — 骨格が安定してから
184
+
185
+ ### インストール
186
+
187
+ ```ruby
188
+ gem 'cordis', github: '5xRuby/cordis-rb' # rubygems.org には未公開
189
+ ```
190
+
191
+ ### 開発
192
+
193
+ ```
194
+ bundle install && bundle exec rspec
195
+ bundle exec ruby examples/demo.rb # ライフサイクル / coeffect ウォークスルー
196
+ ruby examples/webapp.rb # 同じストーリーを Sinatra + Falcon + SQLite で
197
+ ```
198
+
199
+ ### ライセンス
200
+
201
+ [Apache-2.0](LICENSE)
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cordis
4
+ # Thin shell: root bootstrap, service access (the paper's coeffect context), and
5
+ # delegation to the plugin/event entry points.
6
+ # Breaking change (round 2): extend now matches upstream semantics — it creates a
7
+ # "same root, different fiber" view of the ctx, not a new disposal scope; new scopes
8
+ # always come from ctx.plugin.
9
+ class Context
10
+ Impl = Struct.new(:name, :fiber, :value)
11
+
12
+ attr_reader :root, :registry, :events, :fiber, :services
13
+
14
+ def initialize
15
+ @root = self
16
+ @services = {} # isolate key => Impl (default key is the name itself)
17
+ @isolate = {} # name => isolation label; absent = the shared default realm
18
+ @intercept = {} # name => per-caller config (copy-on-write, chain pre-merged)
19
+ @fiber = Fiber.new(self) # root fiber: always active, dispose = restart
20
+ @registry = Registry.new(self)
21
+ @events = Events.new(self)
22
+ end
23
+
24
+ # Internal: a ctx view bound to another fiber (sharing root/registry/events/services).
25
+ def extend(fiber: @fiber)
26
+ ctx = dup
27
+ ctx.instance_variable_set(:@fiber, fiber)
28
+ ctx
29
+ end
30
+
31
+ # A ctx view where service `name` lives in its own realm: provides/injects through
32
+ # this view no longer see (or are seen by) the default realm. Pass the same label
33
+ # to two isolate calls to share one realm between them.
34
+ def isolate(name, label = nil)
35
+ ctx = extend
36
+ ctx.instance_variable_set(:@isolate, @isolate.merge(name.to_sym => label || Object.new))
37
+ ctx
38
+ end
39
+
40
+ # A ctx view carrying per-caller config for service `name` (upstream ctx.intercept).
41
+ # Nested intercepts merge, inner overriding outer (Hash configs only; anything else replaces).
42
+ def intercept(name, config)
43
+ name = name.to_sym
44
+ old = @intercept[name]
45
+ merged = old.is_a?(Hash) && config.is_a?(Hash) ? old.merge(config) : config
46
+ ctx = extend
47
+ ctx.instance_variable_set(:@intercept, @intercept.merge(name => merged))
48
+ ctx
49
+ end
50
+
51
+ # Effective intercept config for `name` as seen from this ctx, merged over `base`
52
+ # (upstream Service[resolveConfig], minus the Config schema merge).
53
+ def resolve_config(name, base = nil)
54
+ config = @intercept[name.to_sym]
55
+ return base if config.nil?
56
+
57
+ base.is_a?(Hash) && config.is_a?(Hash) ? base.merge(config) : config
58
+ end
59
+
60
+ # The store key for `name` in this ctx's realm.
61
+ def isolate_key(name) = @isolate[name] || name
62
+
63
+ # -- effect / plugin --
64
+
65
+ def effect(label = nil, &) = @fiber.effect(label, &)
66
+ def plugin(plugin, config = nil) = @root.registry.plugin(self, plugin, config)
67
+
68
+ def inject(deps, &block)
69
+ plugin({ inject: deps, apply: block, name: 'inject' })
70
+ end
71
+
72
+ # -- events --
73
+
74
+ def on(name, prepend: false, &) = @root.events.register(self, name, prepend: prepend, &)
75
+ def once(name, prepend: false, &) = @root.events.once(self, name, prepend: prepend, &)
76
+ def emit(name, *) = @root.events.emit(name, *)
77
+ def bail(name, *) = @root.events.bail(name, *)
78
+ def serial(name, *) = @root.events.serial(name, *)
79
+ def parallel(name, *) = @root.events.parallel(name, *)
80
+ def waterfall(name, *, &) = @root.events.waterfall(name, *, &)
81
+
82
+ # -- service(reactive coeffect)--
83
+
84
+ # Providing a service is itself a revertible effect.
85
+ # Teardown ordering guarantee: remove from the store -> notify -> wait for every
86
+ # dependent to finish unloading -> only then delete the provider's self-visible entry
87
+ # (so dependents' disposers can still reach the service during their own teardown).
88
+ def provide(name, value = nil)
89
+ name = name.to_sym
90
+ key = isolate_key(name)
91
+ owner = @fiber
92
+ @fiber.effect("ctx.provide(#{name.inspect})") do
93
+ raise ServiceError, "service #{name.inspect} has already been registered" if @root.services.key?(key)
94
+
95
+ impl = Impl.new(name, owner, value)
96
+ @root.services[key] = impl
97
+ owner.store[name] = impl # immediately visible to the provider itself
98
+ owner.provided[name] = key
99
+ @root.notify({ name => key }) if owner.state == :active
100
+ lambda do
101
+ @root.services.delete(key)
102
+ dependents = @root.notify({ name => key })
103
+ dependents.each do |dep|
104
+ dep.await
105
+ rescue StandardError
106
+ nil # allSettled semantics: one dependent failing to unload never blocks the rest
107
+ end
108
+ owner.store&.delete(name)
109
+ owner.provided.delete(name)
110
+ end
111
+ end
112
+ end
113
+
114
+ # Strict: the provider fiber must be ACTIVE to be visible (a service whose provider
115
+ # is still loading is invisible to dependents).
116
+ def get(name)
117
+ impl = @root.services[isolate_key(name.to_sym)]
118
+ return nil unless impl && impl.fiber.state == :active
119
+
120
+ impl.value
121
+ end
122
+
123
+ def set(name, value)
124
+ name = name.to_sym
125
+ impl = @root.services[isolate_key(name)]
126
+ raise ServiceError, %(cannot set property "#{name}" without provide) unless impl
127
+ raise ServiceError, %(cannot set property "#{name}" in multiple fibers) unless impl.fiber.equal?(@fiber)
128
+
129
+ impl.value = value
130
+ end
131
+
132
+ # Dependency-graph update (fully synchronous): linear scan over all fibers,
133
+ # re-checking satisfaction and recomputing epochs. Only fibers whose ctx resolves
134
+ # the name to the same isolate key are touched. Takes { name => key } pairs and
135
+ # returns the touched fibers (provide's teardown uses the list to wait for dependents).
136
+ def notify(pairs)
137
+ touched = []
138
+ @registry.runtimes.each do |runtime|
139
+ runtime.fibers.each do |fib|
140
+ hits = pairs.select { |n, key| fib.inject.key?(n) && fib.ctx.isolate_key(n) == key }
141
+ next if hits.empty?
142
+
143
+ hits.each_key { |n| fib.check_impl(n) }
144
+ fib.refresh
145
+ touched << fib
146
+ end
147
+ end
148
+ pairs.each_key { |n| @events.emit('internal/service', n) }
149
+ touched
150
+ end
151
+
152
+ # Service access: walk the fiber parent chain looking at snapshots
153
+ # (error messages match upstream).
154
+ def method_missing(name, *args, &block)
155
+ return super unless args.empty? && block.nil?
156
+
157
+ resolve_service(name)
158
+ end
159
+
160
+ def respond_to_missing?(name, include_private = false)
161
+ @root.services.key?(isolate_key(name)) || super
162
+ end
163
+
164
+ private
165
+
166
+ def resolve_service(name)
167
+ key = isolate_key(name)
168
+ fib = @fiber
169
+ loop do
170
+ if fib.store
171
+ impl = fib.store[name]
172
+ # a service this fiber *provides* is only visible from the same realm
173
+ # (stricter than upstream, whose walk is name-keyed at the terminal fiber)
174
+ return impl.value if impl && (!fib.provided.key?(name) || fib.provided[name] == key)
175
+ end
176
+ raise ServiceError, %(cannot get required service "#{name}" in inactive context) if fib.inject.key?(name)
177
+ # stop at root, or when walking up would cross an isolation boundary
178
+ if fib.root? || fib.parent_ctx.isolate_key(name) != key
179
+ raise ServiceError, %(cannot get property "#{name}" without inject)
180
+ end
181
+
182
+ fib = fib.parent_fiber
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+
5
+ module Cordis
6
+ # Event system (upstream 4.0's EventsService): registering a listener is itself a
7
+ # fiber effect, so listeners are removed automatically when the fiber unloads.
8
+ # emit/bail/waterfall are synchronous; parallel requires a reactor.
9
+ class Events
10
+ Hook = Struct.new(:ctx, :callback)
11
+
12
+ attr_reader :hooks
13
+
14
+ def initialize(ctx)
15
+ @ctx = ctx
16
+ @hooks = Hash.new { |hash, key| hash[key] = [] }
17
+ end
18
+
19
+ def register(ctx, name, prepend: false, &callback)
20
+ list = @hooks[name]
21
+ hook = Hook.new(ctx, callback)
22
+ ctx.fiber.effect("ctx.on(#{name.inspect})") do
23
+ prepend ? list.unshift(hook) : list.push(hook)
24
+ -> { list.reject! { |h| h.equal?(hook) } }
25
+ end
26
+ end
27
+
28
+ def once(ctx, name, prepend: false, &callback)
29
+ dispose = register(ctx, name, prepend: prepend) do |*args|
30
+ dispose.call
31
+ callback.call(*args)
32
+ end
33
+ end
34
+
35
+ # Synchronous fan-out; return values ignored; listener exceptions propagate (matches upstream).
36
+ def emit(name, *)
37
+ @hooks[name].dup.each { |hook| hook.callback.call(*) }
38
+ nil
39
+ end
40
+
41
+ # Sequential calls; returns the first non-nil/false result.
42
+ # serial is the same semantics named for async listeners: Ruby async is blocking-style,
43
+ # a listener just blocks, so the two share one implementation.
44
+ def bail(name, *)
45
+ @hooks[name].dup.each do |hook|
46
+ result = hook.callback.call(*)
47
+ return result if bailed?(result)
48
+ end
49
+ nil
50
+ end
51
+ alias serial bail
52
+
53
+ # Middleware chain: each listener receives an extra next (callable); not calling
54
+ # next stops the chain; the fallback block runs only when every listener passed through.
55
+ def waterfall(name, *, &fallback)
56
+ chain = @hooks[name].dup
57
+ index = -1
58
+ step = nil
59
+ step = lambda do |*current|
60
+ index += 1
61
+ if index < chain.size
62
+ chain[index].callback.call(*current, step)
63
+ elsif fallback
64
+ fallback.call(*current)
65
+ end
66
+ end
67
+ step.call(*)
68
+ end
69
+
70
+ # Call all listeners concurrently without short-circuiting: exceptions are collected
71
+ # into an AggregateError after everything ran (requires Sync/Async).
72
+ def parallel(name, *)
73
+ parent = Async::Task.current
74
+ tasks = @hooks[name].dup.map do |hook|
75
+ parent.async { hook.callback.call(*) }
76
+ end
77
+ errors = []
78
+ tasks.each do |task|
79
+ task.wait
80
+ rescue StandardError => e
81
+ errors << e
82
+ end
83
+ raise AggregateError, errors unless errors.empty?
84
+
85
+ nil
86
+ end
87
+
88
+ private
89
+
90
+ def bailed?(result) = !result.nil? && result != false
91
+ end
92
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+
5
+ module Cordis
6
+ # Fiber (upstream 4.0's new name for EffectScope): the lifecycle unit of a plugin,
7
+ # and also the collector of revertible effects (the paper's phi accumulator).
8
+ # Note: the clash with Ruby core's ::Fiber is deliberate, to match upstream naming;
9
+ # Cordis never uses core Fiber directly.
10
+ class Fiber
11
+ INACTIVE = :__inactive__
12
+
13
+ Entry = Struct.new(:disposers, :label)
14
+
15
+ attr_reader :uid, :ctx, :runtime, :inject, :store, :config, :error, :provided, :parent_fiber, :parent_ctx,
16
+ :state
17
+
18
+ def initialize(parent_ctx, config: nil, inject: {}, runtime: nil)
19
+ @config = config
20
+ @inject = inject
21
+ @runtime = runtime
22
+ @disposables = []
23
+ @provided = {} # name => isolate key (for boundary-crossing notify on state change)
24
+ @internal_store = {} # currently satisfied dependencies (name => Impl), continuously updated
25
+ @store = nil # snapshot taken while loading; nil means unloaded
26
+ @current_epoch = INACTIVE
27
+ @target_epoch = INACTIVE
28
+ @inertia = nil # in-flight load/unload transition task
29
+ @error = nil
30
+ @state = :pending
31
+
32
+ if runtime.nil? # root fiber
33
+ @uid = 0
34
+ @ctx = parent_ctx
35
+ @parent_fiber = nil
36
+ @current_epoch = @target_epoch = ''
37
+ @store = {}
38
+ @state = :active
39
+ return
40
+ end
41
+
42
+ @uid = parent_ctx.root.registry.next_uid
43
+ @parent_fiber = parent_ctx.fiber
44
+ @parent_ctx = parent_ctx
45
+ @ctx = parent_ctx.extend(fiber: self)
46
+ # inject with a Hash config doubles as an intercept entry on the plugin's ctx
47
+ # (mirrors upstream fiber.ts:139)
48
+ inject.each { |dep, cfg| @ctx = @ctx.intercept(dep, cfg) if cfg.is_a?(Hash) }
49
+ runtime.fibers << self
50
+ @ctx.events.emit('internal/plugin', self)
51
+ inject.each_key { |name| check_impl(name) }
52
+ # a plugin is itself a revertible effect on the parent fiber (mirrors upstream fiber.ts:170)
53
+ @entry = parent_ctx.fiber.effect('ctx.plugin()') do
54
+ refresh
55
+ -> { terminate }
56
+ end
57
+ end
58
+
59
+ def root? = @runtime.nil?
60
+ def disposed? = @uid.nil?
61
+
62
+ def name
63
+ return 'root' if root?
64
+
65
+ @runtime.name || 'plugin'
66
+ end
67
+
68
+ # Apply a side effect and collect its inverse. The block returns: nil (nothing),
69
+ # a callable (single disposer), or an Array/Enumerator of callables (multi-step
70
+ # disposers, reverted in reverse order — upstream's function* effects).
71
+ # If setup raises midway, already-collected disposers are rolled back in reverse, then re-raised.
72
+ def effect(label = nil, &block)
73
+ assert_active
74
+ disposers = collect_disposers(block, rollback: true)
75
+ entry = Entry.new(disposers, label)
76
+ @disposables << entry
77
+ lambda do
78
+ next unless @disposables.reject! { |e| e.equal?(entry) }
79
+
80
+ entry.disposers.reverse_each(&:call)
81
+ end
82
+ end
83
+
84
+ def assert_active
85
+ return if @store && %i[loading active].include?(@state)
86
+
87
+ raise InactiveEffectError, 'cannot create effect on inactive context'
88
+ end
89
+
90
+ # Wait for all in-flight transitions to finish (while, not if: transitions chain).
91
+ def await
92
+ while (task = @inertia)
93
+ task.wait
94
+ end
95
+ raise @error if @error
96
+
97
+ self
98
+ end
99
+
100
+ def dispose
101
+ return restart if root?
102
+
103
+ @entry.call
104
+ nil
105
+ end
106
+
107
+ def restart
108
+ set_epoch(INACTIVE)
109
+ await # wait for the unload first (refreshing right away would set the target back
110
+ # to the original epoch and coalesce the unload intent away)
111
+ refresh
112
+ await
113
+ end
114
+
115
+ def update(config)
116
+ @config = config
117
+ restart
118
+ end
119
+
120
+ # -- coeffect machinery (dependency-graph updates are fully synchronous;
121
+ # only load/unload execution is async) --
122
+
123
+ def check_impl(name)
124
+ impl = @ctx.root.services[@ctx.isolate_key(name)]
125
+ if impl && impl.fiber.state == :active
126
+ @internal_store[name] = impl
127
+ else
128
+ @internal_store.delete(name)
129
+ end
130
+ end
131
+
132
+ def refresh
133
+ epoch = ''
134
+ @inject.each_key do |name|
135
+ impl = @internal_store[name]
136
+ if impl.nil?
137
+ epoch = INACTIVE
138
+ break
139
+ end
140
+ epoch = "#{epoch}:#{impl.fiber.uid}"
141
+ end
142
+ set_epoch(epoch)
143
+ end
144
+
145
+ private
146
+
147
+ def collect_disposers(block, rollback: false)
148
+ disposers = []
149
+ result = block.call
150
+ case result
151
+ when nil then nil
152
+ when Array, Enumerator
153
+ # collect incrementally (never force the whole enumeration first): if it raises
154
+ # midway, only disposers yielded so far can be rolled back
155
+ result.each do |disposer|
156
+ validate_disposer(disposer)
157
+ disposers << disposer
158
+ end
159
+ else
160
+ validate_disposer(result)
161
+ disposers << result
162
+ end
163
+ disposers
164
+ rescue StandardError
165
+ disposers.reverse_each { |d| safe_call(d) } if rollback
166
+ raise
167
+ end
168
+
169
+ def validate_disposer(disposer)
170
+ return if disposer.respond_to?(:call)
171
+
172
+ raise TypeError, "invalid effect: disposer must be callable, got #{disposer.inspect}"
173
+ end
174
+
175
+ def safe_call(disposer)
176
+ disposer.call
177
+ rescue StandardError => e
178
+ warn "Cordis: error while disposing (#{name}): #{e.class}: #{e.message}"
179
+ end
180
+
181
+ def set_epoch(epoch)
182
+ return if epoch == @target_epoch
183
+
184
+ @target_epoch = epoch
185
+ return if @inertia # transition in flight: just record the intent; re-checked on completion
186
+
187
+ start_transition
188
+ end
189
+
190
+ # Inertia lock: at most one transition task per fiber at a time; on completion the
191
+ # target epoch is re-checked and, if it changed, the next transition is chained.
192
+ def start_transition
193
+ task = Async::Task.current # raises outside a reactor (plugin/provide require Sync/Async)
194
+ set_state(@current_epoch == INACTIVE ? :loading : :unloading)
195
+ @inertia = task.async do
196
+ Async::Task.current.yield # defer by one tick (upstream's await Promise.resolve())
197
+ loop do
198
+ if @current_epoch == INACTIVE
199
+ break if @target_epoch == INACTIVE
200
+
201
+ do_load
202
+ else
203
+ break if @target_epoch == @current_epoch
204
+
205
+ do_unload
206
+ end
207
+ end
208
+ @inertia = nil
209
+ set_state(resting_state)
210
+ end
211
+ end
212
+
213
+ def do_load
214
+ goal = @target_epoch
215
+ @error = nil
216
+ @store = @internal_store.dup # snapshot: a loading plugin sees a frozen view of its deps
217
+ set_state(:loading)
218
+ begin
219
+ result = @runtime.callback.call(@ctx, @config) unless root?
220
+ # ponytail: a plugin return value only counts as a disposer when callable
221
+ # (upstream _execute is stricter and raises TypeError)
222
+ @disposables << Entry.new([result], 'plugin:return') if result.respond_to?(:call)
223
+ rescue StandardError => e
224
+ @error = e
225
+ warn "Cordis: plugin #{name} failed to load: #{e.class}: #{e.message}"
226
+ end
227
+ @current_epoch = goal
228
+ end
229
+
230
+ def do_unload
231
+ set_state(:unloading)
232
+ # ponytail: strict LIFO sequential disposal, each rescued individually
233
+ # (upstream starts in LIFO order but finishes concurrently)
234
+ until @disposables.empty?
235
+ entry = @disposables.pop
236
+ entry.disposers.reverse_each { |d| safe_call(d) }
237
+ end
238
+ @store = nil
239
+ @error = nil
240
+ @current_epoch = INACTIVE
241
+ end
242
+
243
+ def resting_state
244
+ return :disposed if disposed?
245
+ return :failed if @error
246
+
247
+ @current_epoch == INACTIVE ? :pending : :active
248
+ end
249
+
250
+ def set_state(new_state)
251
+ return if new_state == @state
252
+
253
+ old = @state
254
+ @state = new_state
255
+ @ctx.events.emit('internal/status', self, old)
256
+ # crossing the ACTIVE boundary changes the visibility of services this fiber
257
+ # provides -> notify dependents
258
+ crossed = (old == :active) ^ (new_state == :active)
259
+ @ctx.root.notify(@provided.dup) if crossed && !@provided.empty?
260
+ end
261
+
262
+ # Called by the parent's effect disposer (or manually via #dispose)
263
+ def terminate
264
+ set_epoch(INACTIVE)
265
+ begin
266
+ await
267
+ rescue StandardError
268
+ nil # disposing a FAILED fiber never raises (the error was already warned at load time)
269
+ end
270
+ @uid = nil
271
+ set_state(:disposed)
272
+ @ctx.root.registry.remove(self)
273
+ @ctx.events.emit('internal/plugin', self)
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cordis
4
+ # Plugin registry: keyed by the resolved callback — one Runtime per callback,
5
+ # applying the same plugin N times = N fibers under one Runtime.
6
+ class Registry
7
+ Runtime = Struct.new(:name, :callback, :fibers, :key)
8
+
9
+ def initialize(ctx)
10
+ @ctx = ctx
11
+ @store = {}
12
+ @counter = 0
13
+ end
14
+
15
+ def next_uid = (@counter += 1)
16
+ def size = @store.size
17
+ def runtimes = @store.values
18
+
19
+ # Entry point for ctx.plugin. Plugin shapes: a callable, { apply:, inject:, name: },
20
+ # or a Class (instantiated at load time; see Cordis::Service).
21
+ # Returns the fiber synchronously; the actual apply is deferred one tick
22
+ # (must be called inside Sync/Async).
23
+ def plugin(ctx, plugin, config = nil)
24
+ key, callback, inject, name = resolve(plugin)
25
+ ctx.fiber.assert_active
26
+ runtime = @store[key] ||= Runtime.new(name, callback, [], key)
27
+ Fiber.new(ctx, config: config, inject: inject, runtime: runtime)
28
+ end
29
+
30
+ def remove(fiber)
31
+ runtime = fiber.runtime
32
+ runtime.fibers.reject! { |f| f.equal?(fiber) }
33
+ @store.delete(runtime.key) if runtime.fibers.empty?
34
+ end
35
+
36
+ private
37
+
38
+ def resolve(plugin)
39
+ case plugin
40
+ when Hash
41
+ apply = plugin[:apply]
42
+ raise ArgumentError, "invalid plugin: apply must be callable, got #{apply.inspect}" \
43
+ unless apply.respond_to?(:call)
44
+
45
+ [apply, apply, normalize_inject(plugin[:inject]), plugin[:name]]
46
+ when Class
47
+ # class plugin (upstream isConstructor): instantiated during load; a Service
48
+ # subclass provides itself in #initialize, then #init runs as the load body
49
+ inject = plugin.respond_to?(:inject) ? plugin.inject : nil
50
+ callback = lambda do |c, config|
51
+ instance = plugin.new(c, config)
52
+ instance.init if instance.respond_to?(:init)
53
+ end
54
+ [plugin, callback, normalize_inject(inject), plugin.name]
55
+ else
56
+ raise ArgumentError, "invalid plugin: #{plugin.inspect}" unless plugin.respond_to?(:call)
57
+
58
+ [plugin, plugin, {}, nil]
59
+ end
60
+ end
61
+
62
+ def normalize_inject(inject)
63
+ case inject
64
+ when nil then {}
65
+ when Array then inject.to_h { |dep| [dep.to_sym, true] }
66
+ when Hash then inject.transform_keys(&:to_sym)
67
+ else raise ArgumentError, "invalid inject: #{inject.inspect}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cordis
4
+ # Class-style plugin that provides itself as a service (upstream Service base class).
5
+ # Declare the name with `provide :foo` and dependencies with `inject`; the instance
6
+ # becomes the service value, and #init (if defined) runs as the plugin load body.
7
+ #
8
+ # class AuditLog < Cordis::Service
9
+ # provide :audit
10
+ # inject :db
11
+ # def init = ctx.on('request') { ... }
12
+ # end
13
+ # ctx.plugin(AuditLog)
14
+ class Service
15
+ class << self
16
+ # DSL setter and registry-facing getter in one (upstream `static provide`).
17
+ def provide(name = nil)
18
+ @service_name = name.to_sym if name
19
+ @service_name
20
+ end
21
+
22
+ # `inject :foo, :bar` or `inject foo: { config }` (upstream `static inject`);
23
+ # called with no arguments it returns the accumulated map for the registry.
24
+ def inject(*names, **configs)
25
+ @inject ||= {}
26
+ names.each { |dep| @inject[dep.to_sym] = true }
27
+ configs.each { |dep, cfg| @inject[dep.to_sym] = cfg }
28
+ @inject
29
+ end
30
+ end
31
+
32
+ attr_reader :ctx, :name, :config
33
+
34
+ def initialize(ctx, config = nil)
35
+ name = self.class.provide
36
+ raise ArgumentError, "#{self.class} must declare `provide :name`" unless name
37
+
38
+ @ctx = ctx
39
+ @config = config
40
+ @name = name
41
+ ctx.provide(name, self)
42
+ end
43
+
44
+ # Load-time hook: runs while the fiber is still loading, so blocking here keeps
45
+ # the service invisible to dependents (upstream's "pending inject" via Service.init).
46
+ # A callable return value is collected as a disposer.
47
+ def init; end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cordis
4
+ VERSION = '0.1.0'
5
+ end
data/lib/cordis.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cordis
4
+ class Error < StandardError; end
5
+
6
+ # Raised when creating an effect / plugin / listener on a non-active context
7
+ class InactiveEffectError < Error; end
8
+
9
+ # Service access violations (without inject / inactive context / duplicate provide / bad set)
10
+ class ServiceError < Error; end
11
+
12
+ # Aggregated errors from parallel (Ruby has no built-in AggregateError)
13
+ class AggregateError < Error
14
+ attr_reader :errors
15
+
16
+ def initialize(errors)
17
+ @errors = errors
18
+ super("#{errors.size} error(s): #{errors.map(&:message).join('; ')}")
19
+ end
20
+ end
21
+ end
22
+
23
+ require_relative 'cordis/version'
24
+ require_relative 'cordis/fiber'
25
+ require_relative 'cordis/registry'
26
+ require_relative 'cordis/events'
27
+ require_relative 'cordis/context'
28
+ require_relative 'cordis/service'
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cordis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ryudoawaru
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: async
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.35'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.35'
26
+ description: Revertible effects with LIFO disposal, a plugin/fiber lifecycle state
27
+ machine, reactive coeffects (service provide/inject), events, and service isolation
28
+ — the core mechanisms of Cordis (cordiverse/cordis), reimplemented on the async
29
+ gem. Based on the paper "A Programming Paradigm for Spatiotemporal Composability".
30
+ email:
31
+ - ryudoawaru@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - LICENSE
37
+ - README.md
38
+ - lib/cordis.rb
39
+ - lib/cordis/context.rb
40
+ - lib/cordis/events.rb
41
+ - lib/cordis/fiber.rb
42
+ - lib/cordis/registry.rb
43
+ - lib/cordis/service.rb
44
+ - lib/cordis/version.rb
45
+ homepage: https://github.com/5xRuby/cordis-rb
46
+ licenses:
47
+ - Apache-2.0
48
+ metadata:
49
+ source_code_uri: https://github.com/5xRuby/cordis-rb
50
+ changelog_uri: https://github.com/5xRuby/cordis-rb/releases
51
+ rubygems_mfa_required: 'true'
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '3.2'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 4.0.16
67
+ specification_version: 4
68
+ summary: A Ruby port of the Cordis plugin/lifecycle meta-framework
69
+ test_files: []