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.
- package/.codesandbox/icon.png +0 -0
- package/.codesandbox/tasks.json +12 -0
- package/.codesandbox/template.json +7 -0
- package/.devcontainer/devcontainer.json +22 -0
- package/.eslintrc.json +12 -0
- package/.tsbuildinfo/build.tsbuildinfo +1 -0
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/build/cjs/index.js +208 -0
- package/build/cjs/index.js.map +1 -0
- package/build/dts/index.d.ts +169 -0
- package/build/dts/index.d.ts.map +1 -0
- package/build/esm/index.js +195 -0
- package/build/esm/index.js.map +1 -0
- package/eslint.config.mjs +121 -0
- package/package.json +77 -0
- package/setupTests.ts +3 -0
- package/src/index.ts +239 -0
- package/test/index.test.ts +3 -0
- package/tsconfig.base.json +40 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +8 -0
- package/tsconfig.src.json +11 -0
- package/tsconfig.test.json +14 -0
- package/vitest.config.ts +17 -0
|
@@ -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
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
|
+
)
|