effer 0.0.1

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.
@@ -0,0 +1,195 @@
1
+ import { Context, Data, Effect, Layer, Queue, Ref, Stream, SubscriptionRef, Unify } from "effect";
2
+ import { NoSuchElementException } from "effect/Cause";
3
+ import { isChannel } from "effect/Channel";
4
+ import { isEffect } from "effect/Effect";
5
+ import { hasProperty } from "effect/Predicate";
6
+ import { StreamTypeId } from "effect/Stream";
7
+ import { html as _html, render as _render } from "lit-html";
8
+ import { AsyncReplaceDirective as _AsyncReplaceDirective, asyncReplace } from "lit-html/directives/async-replace.js";
9
+ import * as Nav from "@typed/navigation";
10
+ import { GetRandomValues } from "@typed/id";
11
+ /**
12
+ * Create an HTML template result that can be rendered to the DOM
13
+ */
14
+ export const html = _html;
15
+ /**
16
+ * Renders a template result to the container
17
+ */
18
+ export const render = _render;
19
+ export class AsyncReplaceDirective extends _AsyncReplaceDirective {}
20
+ const isQueue = val => hasProperty(val, Unify.ignoreSymbol) && hasProperty(val[Unify.ignoreSymbol], 'Dequeue');
21
+ const isStream = val => hasProperty(val, StreamTypeId);
22
+ const isSubscriptionRef = val => hasProperty(val, SubscriptionRef.SubscriptionRefTypeId);
23
+ /**
24
+ * The Effer service provides methods for attaching Attachable values to the template and queueing events
25
+ */
26
+ export class Effer extends /*#__PURE__*/Effect.Service()('Effer', {
27
+ effect: /*#__PURE__*/Effect.gen(function* () {
28
+ const attach = val => {
29
+ let stream;
30
+ if (isChannel(val)) {
31
+ stream = Stream.fromChannel(val);
32
+ } else if (isSubscriptionRef(val)) {
33
+ stream = val.changes;
34
+ } else if (isEffect(val) && !isQueue(val)) {
35
+ stream = Stream.fromEffect(val);
36
+ } else if (isQueue(val)) {
37
+ stream = Stream.fromQueue(val);
38
+ } else if (isStream(val)) {
39
+ stream = val;
40
+ } else {
41
+ stream = Stream.fromPubSub(val);
42
+ }
43
+ return stream.pipe(Stream.toAsyncIterableEffect, Effect.andThen(iter => asyncReplace(iter)));
44
+ };
45
+ const queueMsg = (offer, mapper) => {
46
+ if (mapper) {
47
+ return e => offer(mapper(e));
48
+ }
49
+ return offer;
50
+ };
51
+ return {
52
+ /**
53
+ * Attaches any Attachable value to the template:
54
+ * ```ts
55
+ * const Counter = () => Effect.gen(function*() {
56
+ * [ countRef, countQueue ] = yield* CounterService // service made with makeReducer or makeState
57
+ *
58
+ * return html`
59
+ * <p>The count is ${yield* attach(countRef)}</p>
60
+ * `
61
+ * })
62
+ * ```
63
+ */
64
+ attach,
65
+ /**
66
+ * Used in place of an event handler callback, this function takes a queue to dispatch messages to,
67
+ * as well as a mapping function from the DOM event to the queue's expected event type
68
+ * ```ts
69
+ * const Counter = () => Effect.gen(function*() {
70
+ * [ countRef, countQueue ] = yield* CounterService // service made with makeReducer
71
+ *
72
+ * return html`
73
+ * <button
74
+ * @click=${queueMsg(countQueue, () => Increment())}
75
+ * > // Increment() is an action defined as part of the CounterService reducer
76
+ * The count is ${yield* attach(countRef)}
77
+ * </button>
78
+ * `
79
+ * })
80
+ * ```
81
+ */
82
+ queueMsg
83
+ };
84
+ })
85
+ // dependencies: [Registry.layer]
86
+ }) {}
87
+ /**
88
+ * Creates a stream of the latest state value, and a queue to update the value.
89
+ * @param initialState The starting state value
90
+ * @param updateFn An effectful function that takes the old state, an update message, and returns a new state
91
+ * @returns A tuple of the stream of the current state value and a queue to dispatch update messages
92
+ *
93
+ * ```ts
94
+ * type Msg = Data.TaggedEnum<{
95
+ * Increment: {};
96
+ * Decrement: {};
97
+ * }>
98
+ * const { Increment, Decrement, $match } = Data.taggedEnum<Msg>()
99
+ * const counterReducer = (state: number, msg: Msg) => $match({
100
+ * Increment: () => Effect.succeed(state + 1),
101
+ * Decrement: () => Effect.succeed(state - 1)
102
+ * })
103
+ *
104
+ * // Inside an Effect
105
+ * const [ countStream, countQueue ] = yield* makeReducer(0, counterReducer)
106
+ * ```
107
+ */
108
+ export const makeReducer = (initialState, updateFn) => Effect.gen(function* () {
109
+ const subRef = yield* SubscriptionRef.make(initialState);
110
+ const updateQueue = yield* Queue.unbounded();
111
+ yield* Effect.gen(function* () {
112
+ const msg = yield* updateQueue.take;
113
+ yield* SubscriptionRef.updateEffect(subRef, state => updateFn(state, msg));
114
+ }).pipe(Effect.forever, Effect.fork);
115
+ const dispatch = msg => Queue.unsafeOffer(updateQueue, msg);
116
+ return {
117
+ stream: subRef.changes,
118
+ dispatch
119
+ };
120
+ });
121
+ export const makeState = initialState => Effect.gen(function* () {
122
+ const subRef = yield* SubscriptionRef.make(initialState);
123
+ const updateQueue = yield* Queue.unbounded();
124
+ yield* Effect.gen(function* () {
125
+ const updateFn = yield* updateQueue.take;
126
+ yield* SubscriptionRef.update(subRef, updateFn);
127
+ }).pipe(Effect.forever, Effect.fork);
128
+ const set = val => Queue.unsafeOffer(updateQueue, _ => val);
129
+ const update = updateFn => Queue.unsafeOffer(updateQueue, updateFn);
130
+ return {
131
+ stream: subRef.changes,
132
+ set,
133
+ update
134
+ };
135
+ });
136
+ /**
137
+ * Effer's service to interact with navigation. Provides the current URL object, a stream of the
138
+ * current app path, a method to get a query param from the URL, and a method to navigate the page.
139
+ */
140
+ export class NavService extends /*#__PURE__*/Context.Tag('NavService')() {
141
+ static Live = /*#__PURE__*/Layer.effect(NavService, /*#__PURE__*/Effect.gen(function* () {
142
+ const url = yield* Ref.make(new URL(window.navigation.currentEntry?.url));
143
+ const pathStream = Stream.fromEventListener(window.navigation, 'navigate').pipe(Stream.map(e => new URL(e.destination.url)), Stream.merge(Stream.make(new URL(window.navigation.currentEntry?.url))), Stream.tap(u => Ref.set(url, u)));
144
+ const getQueryParam = name => Effect.gen(function* () {
145
+ const result = (yield* Ref.get(url)).searchParams.get(name);
146
+ if (result === null) {
147
+ yield* Effect.fail(new NoSuchElementException());
148
+ }
149
+ return result;
150
+ });
151
+ return {
152
+ /**
153
+ * The current URL object
154
+ */
155
+ url,
156
+ /**
157
+ * A stream of the current app path ('/', '/actuator', etc.)
158
+ */
159
+ pathStream,
160
+ /**
161
+ * Method used to navigate. Accepts a URL string and navigates the page
162
+ */
163
+ navigate: window.navigation.navigate,
164
+ /**
165
+ * Get a value from the current URL's query params
166
+ */
167
+ getQueryParam
168
+ };
169
+ }));
170
+ }
171
+ export const makeAsyncResult = effect => {
172
+ const {
173
+ Loading,
174
+ Success,
175
+ Failure,
176
+ $is,
177
+ $match
178
+ } = Data.taggedEnum();
179
+ const startStream = Stream.make(Loading());
180
+ const resultStream = Stream.asyncPush(emit => Effect.gen(function* () {
181
+ const result = yield* effect.pipe(Effect.map(data => Success({
182
+ data
183
+ })), Effect.mapError(error => Failure({
184
+ error
185
+ })), Effect.merge);
186
+ emit.single(result);
187
+ }));
188
+ return {
189
+ stream: Stream.concat(startStream, resultStream),
190
+ is: $is,
191
+ match: $match
192
+ };
193
+ };
194
+ export const layer = /*#__PURE__*/Effer.Default.pipe(/*#__PURE__*/Layer.provideMerge(NavService.Live), /*#__PURE__*/Layer.provide(/*#__PURE__*/Nav.fromWindow(window)), /*#__PURE__*/Layer.provide(GetRandomValues.CryptoRandom));
195
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["Context","Data","Effect","Layer","Queue","Ref","Stream","SubscriptionRef","Unify","NoSuchElementException","isChannel","isEffect","hasProperty","StreamTypeId","html","_html","render","_render","AsyncReplaceDirective","_AsyncReplaceDirective","asyncReplace","Nav","GetRandomValues","isQueue","val","ignoreSymbol","isStream","isSubscriptionRef","SubscriptionRefTypeId","Effer","Service","effect","gen","attach","stream","fromChannel","changes","fromEffect","fromQueue","fromPubSub","pipe","toAsyncIterableEffect","andThen","iter","queueMsg","offer","mapper","e","makeReducer","initialState","updateFn","subRef","make","updateQueue","unbounded","msg","take","updateEffect","state","forever","fork","dispatch","unsafeOffer","makeState","update","set","_","NavService","Tag","Live","url","URL","window","navigation","currentEntry","pathStream","fromEventListener","map","destination","merge","tap","u","getQueryParam","name","result","get","searchParams","fail","navigate","makeAsyncResult","Loading","Success","Failure","$is","$match","taggedEnum","startStream","resultStream","asyncPush","emit","data","mapError","error","single","concat","is","match","layer","Default","provideMerge","provide","fromWindow","CryptoRandom"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,121 @@
1
+ import * as effectEslint from "@effect/eslint-plugin"
2
+ import { fixupPluginRules } from "@eslint/compat"
3
+ import { FlatCompat } from "@eslint/eslintrc"
4
+ import js from "@eslint/js"
5
+ import tsParser from "@typescript-eslint/parser"
6
+ import codegen from "eslint-plugin-codegen"
7
+ import _import from "eslint-plugin-import"
8
+ import simpleImportSort from "eslint-plugin-simple-import-sort"
9
+ import sortDestructureKeys from "eslint-plugin-sort-destructure-keys"
10
+ import path from "node:path"
11
+ import { fileURLToPath } from "node:url"
12
+
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = path.dirname(__filename)
15
+ const compat = new FlatCompat({
16
+ baseDirectory: __dirname,
17
+ recommendedConfig: js.configs.recommended,
18
+ allConfig: js.configs.all
19
+ })
20
+
21
+ export default [
22
+ {
23
+ ignores: ["**/dist", "**/build", "**/docs", "**/*.md"]
24
+ },
25
+ ...compat.extends(
26
+ "eslint:recommended",
27
+ "plugin:@typescript-eslint/eslint-recommended",
28
+ "plugin:@typescript-eslint/recommended"
29
+ ),
30
+ ...effectEslint.configs.dprint,
31
+ {
32
+ plugins: {
33
+ import: fixupPluginRules(_import),
34
+ "sort-destructure-keys": sortDestructureKeys,
35
+ "simple-import-sort": simpleImportSort,
36
+ codegen
37
+ },
38
+
39
+ languageOptions: {
40
+ parser: tsParser,
41
+ ecmaVersion: 2018,
42
+ sourceType: "module"
43
+ },
44
+
45
+ settings: {
46
+ "import/parsers": {
47
+ "@typescript-eslint/parser": [".ts", ".tsx"]
48
+ },
49
+
50
+ "import/resolver": {
51
+ typescript: {
52
+ alwaysTryTypes: true
53
+ }
54
+ }
55
+ },
56
+
57
+ rules: {
58
+ "codegen/codegen": "error",
59
+ "no-fallthrough": "off",
60
+ "no-irregular-whitespace": "off",
61
+ "object-shorthand": "error",
62
+ "prefer-destructuring": "off",
63
+ "sort-imports": "off",
64
+
65
+ "no-restricted-syntax": ["error", {
66
+ selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments",
67
+ message: "Do not use spread arguments in Array.push"
68
+ }],
69
+
70
+ "no-unused-vars": "off",
71
+ "prefer-rest-params": "off",
72
+ "prefer-spread": "off",
73
+ "import/first": "error",
74
+ "import/newline-after-import": "error",
75
+ "import/no-duplicates": "error",
76
+ "import/no-unresolved": "off",
77
+ "import/order": "off",
78
+ "simple-import-sort/imports": "off",
79
+ "sort-destructure-keys/sort-destructure-keys": "error",
80
+ "deprecation/deprecation": "off",
81
+
82
+ "@typescript-eslint/array-type": ["warn", {
83
+ default: "generic",
84
+ readonly: "generic"
85
+ }],
86
+
87
+ "@typescript-eslint/member-delimiter-style": 0,
88
+ "@typescript-eslint/no-non-null-assertion": "off",
89
+ "@typescript-eslint/ban-types": "off",
90
+ "@typescript-eslint/no-explicit-any": "off",
91
+ "@typescript-eslint/no-empty-interface": "off",
92
+ "@typescript-eslint/consistent-type-imports": "warn",
93
+
94
+ "@typescript-eslint/no-unused-vars": ["error", {
95
+ argsIgnorePattern: "^_",
96
+ varsIgnorePattern: "^_"
97
+ }],
98
+
99
+ "@typescript-eslint/ban-ts-comment": "off",
100
+ "@typescript-eslint/camelcase": "off",
101
+ "@typescript-eslint/explicit-function-return-type": "off",
102
+ "@typescript-eslint/explicit-module-boundary-types": "off",
103
+ "@typescript-eslint/interface-name-prefix": "off",
104
+ "@typescript-eslint/no-array-constructor": "off",
105
+ "@typescript-eslint/no-use-before-define": "off",
106
+ "@typescript-eslint/no-namespace": "off",
107
+
108
+ "@effect/dprint": ["error", {
109
+ config: {
110
+ indentWidth: 2,
111
+ lineWidth: 120,
112
+ semiColons: "asi",
113
+ quoteStyle: "alwaysDouble",
114
+ trailingCommas: "never",
115
+ operatorPosition: "maintain",
116
+ "arrowFunction.useParentheses": "force"
117
+ }
118
+ }]
119
+ }
120
+ }
121
+ ]
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "effer",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "An Effect native UI library based on Lit-HTML",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/lself1022/effer"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "directory": "dist"
14
+ },
15
+ "exports": {
16
+ "./package.json": "./package.json",
17
+ ".": "./src/index.ts",
18
+ "./*": "./src/*.ts"
19
+ },
20
+ "scripts": {
21
+ "codegen": "build-utils prepare-v2",
22
+ "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2",
23
+ "build-esm": "tsc -b tsconfig.build.json",
24
+ "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps",
25
+ "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps",
26
+ "check": "tsc -b tsconfig.json",
27
+ "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"",
28
+ "lint-fix": "pnpm lint --fix",
29
+ "test": "vitest",
30
+ "coverage": "vitest --coverage"
31
+ },
32
+ "dependencies": {
33
+ "effect": "^3.18.4",
34
+ "@typed/id": "^0.17.2",
35
+ "@typed/navigation": "^0.18.1",
36
+ "lit-html": "^3.3.1"
37
+ },
38
+ "devDependencies": {
39
+ "@babel/cli": "^7.24.8",
40
+ "@babel/core": "^7.25.2",
41
+ "@babel/plugin-transform-export-namespace-from": "^7.24.7",
42
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
43
+ "@effect/build-utils": "^0.8.9",
44
+ "@effect/eslint-plugin": "^0.3.2",
45
+ "@effect/language-service": "latest",
46
+ "@effect/vitest": "^0.25.1",
47
+ "@eslint/compat": "1.1.1",
48
+ "@eslint/eslintrc": "3.1.0",
49
+ "@eslint/js": "9.10.0",
50
+ "@types/node": "^22.5.2",
51
+ "@types/dom-navigation": "^1.0.6",
52
+ "@typescript-eslint/eslint-plugin": "^8.4.0",
53
+ "@typescript-eslint/parser": "^8.4.0",
54
+ "babel-plugin-annotate-pure-calls": "^0.5.0",
55
+ "eslint": "^9.10.0",
56
+ "eslint-import-resolver-typescript": "^3.6.3",
57
+ "eslint-plugin-codegen": "^0.28.0",
58
+ "eslint-plugin-import": "^2.30.0",
59
+ "eslint-plugin-simple-import-sort": "^12.1.1",
60
+ "eslint-plugin-sort-destructure-keys": "^2.0.0",
61
+ "tsx": "^4.17.0",
62
+ "typescript": "^5.6.2",
63
+ "vitest": "^3.2.0"
64
+ },
65
+ "effect": {
66
+ "generateExports": {
67
+ "include": [
68
+ "**/*.ts"
69
+ ]
70
+ },
71
+ "generateIndex": {
72
+ "include": [
73
+ "**/*.ts"
74
+ ]
75
+ }
76
+ }
77
+ }
package/setupTests.ts ADDED
@@ -0,0 +1,3 @@
1
+ import * as it from "@effect/vitest"
2
+
3
+ it.addEqualityTesters()
package/src/index.ts ADDED
@@ -0,0 +1,239 @@
1
+ import { Chunk, Console, Context, Data, Effect, Layer, Queue, Ref, Stream, SubscriptionRef, Unify } from "effect";
2
+ import { NoSuchElementException } from "effect/Cause";
3
+ import { type Channel, isChannel } from "effect/Channel";
4
+ import { isEffect } from "effect/Effect";
5
+ import { hasProperty } from "effect/Predicate";
6
+ import { type PubSub } from "effect/PubSub";
7
+ import { StreamTypeId } from "effect/Stream";
8
+ import { type TemplateResult as _TemplateResult, html as _html, render as _render } from "lit-html";
9
+ import type { DirectiveClass, DirectiveResult as _DirectiveResult } from "lit-html/directive.js";
10
+ import { AsyncReplaceDirective as _AsyncReplaceDirective, asyncReplace } from "lit-html/directives/async-replace.js";
11
+ import * as Nav from "@typed/navigation";
12
+ import { GetRandomValues } from "@typed/id";
13
+
14
+ /**
15
+ * Create an HTML template result that can be rendered to the DOM
16
+ */
17
+ export const html = _html
18
+ /**
19
+ * Renders a template result to the container
20
+ */
21
+ export const render = _render
22
+
23
+ export type TemplateResult = _TemplateResult
24
+ export type DirectiveResult<C extends DirectiveClass = DirectiveClass> = _DirectiveResult<C>
25
+ export class AsyncReplaceDirective extends _AsyncReplaceDirective {}
26
+
27
+ /**
28
+ * Types that can be attached to the DOM template using Effer's 'attach' method
29
+ */
30
+ export type Attachable<A,E,R> =
31
+ | Channel<Chunk.Chunk<A>, unknown, E, unknown, unknown, unknown, R>
32
+ | Effect.Effect<A,E,R>
33
+ | SubscriptionRef.SubscriptionRef<A>
34
+ | PubSub<A>
35
+ | Queue.Queue<A>
36
+ | Stream.Stream<A,E,R>
37
+ const isQueue= <A>(val: unknown): val is Queue.Queue<A> => hasProperty(val, Unify.ignoreSymbol) && hasProperty(val[Unify.ignoreSymbol], 'Dequeue')
38
+ const isStream = <A,E,R>(val: unknown): val is Stream.Stream<A,E,R> => hasProperty(val, StreamTypeId)
39
+ const isSubscriptionRef = <A>(val: unknown): val is SubscriptionRef.SubscriptionRef<A> => hasProperty(val, SubscriptionRef.SubscriptionRefTypeId)
40
+
41
+ /**
42
+ * The Effer service provides methods for attaching Attachable values to the template and queueing events
43
+ */
44
+ export class Effer extends Effect.Service<Effer>()('Effer', {
45
+ effect: Effect.gen(function*() {
46
+ const attach = <A,E=never,R=never>(val: Attachable<A,E,R>) => {
47
+ let stream: Stream.Stream<A,E,R>;
48
+ if(isChannel(val)) {
49
+ stream = Stream.fromChannel<A,E,R>(val)
50
+ } else if (isSubscriptionRef<A>(val)) {
51
+ stream = val.changes
52
+ } else if(isEffect(val) && !isQueue<A>(val)) {
53
+ stream = Stream.fromEffect(val)
54
+ } else if(isQueue<A>(val)) {
55
+ stream = Stream.fromQueue(val)
56
+ } else if(isStream<A,E,R>(val)) {
57
+ stream = val
58
+ } else {
59
+ stream = Stream.fromPubSub(val)
60
+ }
61
+ return stream.pipe(
62
+ Stream.toAsyncIterableEffect,
63
+ Effect.andThen(iter => asyncReplace(iter))
64
+ )
65
+ }
66
+ const queueMsg = <DOMEvent, Msg>(offer: (msg: Msg) => boolean, mapper?: (e: DOMEvent) => Msg) => {
67
+ if (mapper) {
68
+ return (e: DOMEvent) => offer(mapper(e));
69
+ }
70
+ return offer;
71
+ }
72
+
73
+ return {
74
+ /**
75
+ * Attaches any Attachable value to the template:
76
+ * ```ts
77
+ * const Counter = () => Effect.gen(function*() {
78
+ * [ countRef, countQueue ] = yield* CounterService // service made with makeReducer or makeState
79
+ *
80
+ * return html`
81
+ * <p>The count is ${yield* attach(countRef)}</p>
82
+ * `
83
+ * })
84
+ * ```
85
+ */
86
+ attach,
87
+ /**
88
+ * Used in place of an event handler callback, this function takes a queue to dispatch messages to,
89
+ * as well as a mapping function from the DOM event to the queue's expected event type
90
+ * ```ts
91
+ * const Counter = () => Effect.gen(function*() {
92
+ * [ countRef, countQueue ] = yield* CounterService // service made with makeReducer
93
+ *
94
+ * return html`
95
+ * <button
96
+ * @click=${queueMsg(countQueue, () => Increment())}
97
+ * > // Increment() is an action defined as part of the CounterService reducer
98
+ * The count is ${yield* attach(countRef)}
99
+ * </button>
100
+ * `
101
+ * })
102
+ * ```
103
+ */
104
+ queueMsg
105
+ }
106
+ }),
107
+ // dependencies: [Registry.layer]
108
+ }) {}
109
+
110
+ /**
111
+ * Creates a stream of the latest state value, and a queue to update the value.
112
+ * @param initialState The starting state value
113
+ * @param updateFn An effectful function that takes the old state, an update message, and returns a new state
114
+ * @returns A tuple of the stream of the current state value and a queue to dispatch update messages
115
+ *
116
+ * ```ts
117
+ * type Msg = Data.TaggedEnum<{
118
+ * Increment: {};
119
+ * Decrement: {};
120
+ * }>
121
+ * const { Increment, Decrement, $match } = Data.taggedEnum<Msg>()
122
+ * const counterReducer = (state: number, msg: Msg) => $match({
123
+ * Increment: () => Effect.succeed(state + 1),
124
+ * Decrement: () => Effect.succeed(state - 1)
125
+ * })
126
+ *
127
+ * // Inside an Effect
128
+ * const [ countStream, countQueue ] = yield* makeReducer(0, counterReducer)
129
+ * ```
130
+ */
131
+ export const makeReducer = <A,M,E=never,R=never>(initialState: A, updateFn: (state: A, msg: M) => Effect.Effect<A,E,R>) => Effect.gen(function*() {
132
+ const subRef = yield* SubscriptionRef.make<A>(initialState)
133
+ const updateQueue = yield* Queue.unbounded<M>()
134
+
135
+ yield* Effect.gen(function*() {
136
+ const msg = yield* updateQueue.take
137
+ yield* SubscriptionRef.updateEffect(subRef, state => updateFn(state, msg))
138
+ }).pipe(
139
+ Effect.forever,
140
+ Effect.fork
141
+ )
142
+ const dispatch = (msg: M) => Queue.unsafeOffer(updateQueue, msg)
143
+
144
+ return {stream: subRef.changes, dispatch}
145
+ })
146
+
147
+ export const makeState = <A>(initialState: A) => Effect.gen(function*() {
148
+ const subRef = yield* SubscriptionRef.make<A>(initialState)
149
+ const updateQueue = yield* Queue.unbounded<(val: A) => A>()
150
+
151
+ yield* Effect.gen(function*() {
152
+ const updateFn = yield* updateQueue.take
153
+ yield* SubscriptionRef.update(subRef, updateFn)
154
+ }).pipe(
155
+ Effect.forever,
156
+ Effect.fork
157
+ )
158
+ const set = (val: A) => Queue.unsafeOffer(updateQueue, _ => val)
159
+ const update = (updateFn: (oldValue: A) => A) => Queue.unsafeOffer(updateQueue,updateFn)
160
+ return {stream: subRef.changes, set, update} as const
161
+ })
162
+
163
+ /**
164
+ * Effer's service to interact with navigation. Provides the current URL object, a stream of the
165
+ * current app path, a method to get a query param from the URL, and a method to navigate the page.
166
+ */
167
+ export class NavService extends Context.Tag('NavService')<
168
+ NavService,
169
+ {
170
+ url: Ref.Ref<URL>;
171
+ pathStream: Stream.Stream<URL>;
172
+ getQueryParam: (name: string) => Effect.Effect<string, NoSuchElementException, never>
173
+ navigate: typeof window.navigation.navigate;
174
+ }
175
+ >() {
176
+ static Live = Layer.effect(NavService, Effect.gen(function*() {
177
+ const url = yield* Ref.make<URL>(new URL(window.navigation.currentEntry?.url!))
178
+ const pathStream = Stream.fromEventListener<NavigateEvent>(window.navigation, 'navigate').pipe(
179
+ Stream.map(e => new URL(e.destination.url)),
180
+ Stream.merge(Stream.make(new URL(window.navigation.currentEntry?.url!))),
181
+ Stream.tap(u => Ref.set(url, u))
182
+ )
183
+ const getQueryParam = (name: string) => Effect.gen(function*() {
184
+ const result: string | null = (yield* Ref.get(url)).searchParams.get(name)
185
+ if(result === null) {
186
+ yield* Effect.fail(new NoSuchElementException())
187
+ }
188
+ return result!
189
+ })
190
+ return {
191
+ /**
192
+ * The current URL object
193
+ */
194
+ url,
195
+ /**
196
+ * A stream of the current app path ('/', '/actuator', etc.)
197
+ */
198
+ pathStream,
199
+ /**
200
+ * Method used to navigate. Accepts a URL string and navigates the page
201
+ */
202
+ navigate: window.navigation.navigate,
203
+ /**
204
+ * Get a value from the current URL's query params
205
+ */
206
+ getQueryParam
207
+ }
208
+ }))
209
+ }
210
+
211
+ export type Result<A,E> = Data.TaggedEnum<{
212
+ Loading: {}
213
+ Success: { readonly data: A }
214
+ Failure: { readonly error: E }
215
+ }> & {}
216
+
217
+ export const makeAsyncResult = <A,E,R>(effect: Effect.Effect<A,E,R>) => {
218
+ const { Loading, Success, Failure, $is, $match } = Data.taggedEnum<Result<A,E>>()
219
+ const startStream = Stream.make(Loading())
220
+ const resultStream: Stream.Stream<Result<A,E>,never,R> = Stream.asyncPush<Result<A,E>,never,R>(
221
+ emit => Effect.gen(function*() {
222
+ const result = yield* effect.pipe(
223
+ Effect.map(data => Success({data})),
224
+ Effect.mapError(error => Failure({error})),
225
+ Effect.merge
226
+ )
227
+ emit.single(result)
228
+ })
229
+ )
230
+
231
+ return {stream: Stream.concat(startStream, resultStream), is: $is, match: $match}
232
+ }
233
+
234
+
235
+ export const layer = Effer.Default.pipe(
236
+ Layer.provideMerge(NavService.Live),
237
+ Layer.provide(Nav.fromWindow(window)),
238
+ Layer.provide(GetRandomValues.CryptoRandom),
239
+ )
@@ -0,0 +1,3 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ describe('')