Compare commits

...

19 Commits

Author SHA1 Message Date
Greg Johnston
49aa5179af fix Suspense to use the old, Effect-like scheduling after fixing RenderEffect 2024-09-11 22:02:04 -04:00
Greg Johnston
20db2094a0 clippy catches my println mistake 2024-08-28 07:40:24 -04:00
Greg Johnston
4f60651633 fix selector to work in all circumstances 2024-08-27 16:00:32 -04:00
Greg Johnston
31fbf738c2 fix isomorphic RenderEffect 2024-08-27 16:00:01 -04:00
Álvaro Mondéjar Rubio
1a4236e100 Init executor 2024-08-27 14:08:57 +02:00
Álvaro Mondéjar Rubio
051a8c8af9 tests: fix Effect doctests not being executed 2024-08-27 13:50:14 +02:00
Marc-Stefan Cassola
58476bb98e feat: variadic From for callbacks (#2873) 2024-08-26 20:47:11 -04:00
Oliver
88d4f14541 added attributes to source element (#2883)
Co-authored-by: Oliver Nordh <oliver.nordh@proton.me>
2024-08-26 19:27:01 -04:00
niklass-l
6bba233ba7 Add support for postcard codec in server_fn (#2879)
* Add support for postcard codec

* Add postcard feature set exclusions

* Add postcard server fn encoding example
2024-08-26 13:58:23 -07:00
Greg Johnston
1d99764740 Merge pull request #2870 from leptos-rs/dependabot/github_actions/tj-actions/changed-files-45
chore(deps): bump tj-actions/changed-files from 44 to 45
2024-08-23 17:36:21 -04:00
Álvaro Mondéjar Rubio
53cc479c14 chore: set tracing as feature for all crates (#2843) 2024-08-23 17:34:05 -04:00
Greg Johnston
d3707d9b88 Merge pull request #2865 from mkane0814/feat/add-map-and-then-for-resource
feat: add Resouce::map and Resouce::and_then
2024-08-23 17:30:57 -04:00
Marc-Stefan Cassola
1df4076fd2 updated codee version (#2874) 2024-08-23 13:50:55 -07:00
dependabot[bot]
28337bb6c9 chore(deps): bump tj-actions/changed-files from 44 to 45
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 44 to 45.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v44...v45)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-21 18:22:38 +00:00
Greg Johnston
c5dea52e69 chore: restore Playwright tests for counters example (#2864) 2024-08-20 13:38:22 -04:00
Greg Johnston
d5096ff2e6 feat: allow including a view from an external file (closes #2182) (#2862) 2024-08-20 09:00:40 -04:00
Matt Kane
84734f1110 feat: add ArcResource::and_then 2024-08-20 07:41:55 -05:00
Matt Kane
7094308287 feat: add Resource::and_then 2024-08-20 00:06:56 -05:00
Matt Kane
3b88c8ccd2 feat: add Resource::map 2024-08-19 23:38:36 -05:00
65 changed files with 1297 additions and 261 deletions

View File

@@ -26,7 +26,7 @@ jobs:
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v45
with:
dir_names: true
dir_names_max_depth: "2"

View File

@@ -21,7 +21,7 @@ jobs:
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v45
with:
files: |
examples/**

View File

@@ -21,7 +21,7 @@ jobs:
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v45
with:
files: |
any_error/**

View File

@@ -28,3 +28,6 @@ futures-executor = ["futures/thread-pool", "futures/executor"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

6
examples/counters/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml

View File

@@ -2,4 +2,5 @@ extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-trunk-test.toml" },
]

4
examples/counters/e2e/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

83
examples/counters/e2e/package-lock.json generated Normal file
View File

@@ -0,0 +1,83 @@
{
"name": "grip",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "grip",
"devDependencies": {
"@playwright/test": "^1.35.1"
}
},
"node_modules/.pnpm/@playwright+test@1.33.0": {
"extraneous": true
},
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
"version": "20.2.1",
"extraneous": true,
"license": "MIT"
},
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
"version": "1.33.0",
"extraneous": true,
"license": "Apache-2.0",
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.35.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/@types/node": {
"version": "20.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright-core": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.46.1"
},
"dependencies": {
"pnpm": "^9.7.1"
}
}

View File

@@ -0,0 +1,77 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !process.env.DEV,
/* Retry on CI only */
retries: process.env.DEV ? 0 : 10,
/* Opt out of parallel tests on CI. */
workers: process.env.DEV ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [["html", { open: "never" }], ["list"]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: "cd ../ && trunk serve",
// url: "http://127.0.0.1:8080",
// reuseExistingServer: false, //!process.env.CI,
// },
});

View File

@@ -0,0 +1,19 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add 1000 Counters", () => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await Promise.all([
await ui.goto(),
await ui.addOneThousandCountersButton.waitFor(),
]);
await ui.addOneThousandCounters();
await ui.addOneThousandCounters();
await ui.addOneThousandCounters();
await expect(ui.counters).toHaveText("3000");
});
});

View File

@@ -0,0 +1,15 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add Counter", () => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await expect(ui.counters).toHaveText("3");
});
});

View File

@@ -0,0 +1,18 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Clear Counters", () => {
test("should reset the counts", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.clearCounters();
await expect(ui.total).toHaveText("0");
await expect(ui.counters).toHaveText("0");
});
});

View File

@@ -0,0 +1,16 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Decrement Count", () => {
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.decrementCount();
await ui.decrementCount();
await ui.decrementCount();
await expect(ui.total).toHaveText("-3");
});
});

View File

@@ -0,0 +1,30 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Enter Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.enterCount("5");
await expect(ui.total).toHaveText("5");
await expect(ui.counters).toHaveText("1");
});
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.enterCount("100");
await ui.enterCount("100", 1);
await ui.enterCount("100", 2);
await ui.enterCount("50", 1);
await expect(ui.total).toHaveText("250");
});
});

View File

@@ -0,0 +1,98 @@
import { expect, Locator, Page } from "@playwright/test";
export class CountersPage {
readonly page: Page;
readonly addCounterButton: Locator;
readonly addOneThousandCountersButton: Locator;
readonly clearCountersButton: Locator;
readonly incrementCountButton: Locator;
readonly counterInput: Locator;
readonly decrementCountButton: Locator;
readonly removeCountButton: Locator;
readonly total: Locator;
readonly counters: Locator;
constructor(page: Page) {
this.page = page;
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
this.addOneThousandCountersButton = page.locator("button", {
hasText: "Add 1000 Counters",
});
this.clearCountersButton = page.locator("button", {
hasText: "Clear Counters",
});
this.decrementCountButton = page.locator("button", {
hasText: "-1",
});
this.incrementCountButton = page.locator("button", {
hasText: "+1",
});
this.removeCountButton = page.locator("button", {
hasText: "x",
});
this.total = page.getByTestId("total");
this.counters = page.getByTestId("counters");
this.counterInput = page.getByRole("textbox");
}
async goto() {
await this.page.goto("/");
}
async addCounter() {
await Promise.all([
this.addCounterButton.waitFor(),
this.addCounterButton.click(),
]);
}
async addOneThousandCounters() {
this.addOneThousandCountersButton.click();
}
async decrementCount(index: number = 0) {
await Promise.all([
this.decrementCountButton.nth(index).waitFor(),
this.decrementCountButton.nth(index).click(),
]);
}
async incrementCount(index: number = 0) {
await Promise.all([
this.incrementCountButton.nth(index).waitFor(),
this.incrementCountButton.nth(index).click(),
]);
}
async clearCounters() {
await Promise.all([
this.clearCountersButton.waitFor(),
this.clearCountersButton.click(),
]);
}
async enterCount(count: string, index: number = 0) {
await Promise.all([
this.counterInput.nth(index).waitFor(),
this.counterInput.nth(index).fill(count),
]);
}
async removeCounter(index: number = 0) {
await Promise.all([
this.removeCountButton.nth(index).waitFor(),
this.removeCountButton.nth(index).click(),
]);
}
}

View File

@@ -0,0 +1,16 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Increment Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.incrementCount();
await ui.incrementCount();
await ui.incrementCount();
await expect(ui.total).toHaveText("3");
});
});

View File

@@ -0,0 +1,17 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Remove Counter", () => {
test("should decrement the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.removeCounter(1);
await expect(ui.counters).toHaveText("2");
});
});

View File

@@ -0,0 +1,19 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("View Counters", () => {
test("should see the title", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await expect(page).toHaveTitle("Counters");
});
test("should see the initial counts", async ({ page }) => {
const counters = new CountersPage(page);
await counters.goto();
await expect(counters.total).toHaveText("0");
await expect(counters.counters).toHaveText("0");
});
});

View File

@@ -2,6 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
<title>Counters</title>
</head>
<body></body>
</html>
</html>

View File

@@ -44,12 +44,13 @@ pub fn Counters() -> impl IntoView {
<button on:click=clear_counters>"Clear Counters"</button>
<p>
"Total: "
<span>
<span data-testid="total">
{move || {
counters.get().iter().map(|(_, count)| count.get()).sum::<i32>().to_string()
}}
</span> " from " <span>{move || counters.get().len().to_string()}</span>
</span> " from "
<span data-testid="counters">{move || counters.get().len().to_string()}</span>
" counters."
</p>
<ul>

View File

@@ -23,7 +23,7 @@ async fn inc() {
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span>0</span> from <span>0</span> counters.</p><ul><!----></ul>"
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
);
// add 3 counters
@@ -38,7 +38,7 @@ async fn inc() {
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span>0</span> from <span>3</span> \
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">3</span> \
counters.</p><ul><li><button>-1</button><input \
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \
@@ -80,7 +80,7 @@ async fn inc() {
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span>6</span> from <span>3</span> \
<span data-testid=\"total\">6</span> from <span data-testid=\"counters\">3</span> \
counters.</p><ul><li><button>-1</button><input \
type=\"text\"><span>1</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \
@@ -105,7 +105,7 @@ async fn inc() {
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: \
<span>5</span> from <span>2</span> \
<span data-testid=\"total\">5</span> from <span data-testid=\"counters\">2</span> \
counters.</p><ul><li><button>-1</button><input \
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
li><li><button>-1</button><input \

View File

@@ -16,6 +16,7 @@ server_fn = { path = "../../server_fn", features = [
"serde-lite",
"rkyv",
"multipart",
"postcard",
] }
log = "0.4.22"
simple_logger = "5.0"

View File

@@ -6,7 +6,8 @@ use server_fn::{
client::{browser::BrowserClient, Client},
codec::{
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
TextStream,
},
request::{browser::BrowserRequest, ClientReq, Req},
response::{browser::BrowserResponse, ClientRes, Res},
@@ -65,6 +66,7 @@ pub fn HomePage() -> impl IntoView {
<h2>"Alternative Encodings"</h2>
<ServerFnArgumentExample/>
<RkyvExample/>
<PostcardExample/>
<FileUpload/>
<FileUploadWithProgress/>
<FileWatcher/>
@@ -880,3 +882,67 @@ pub fn CustomClientExample() -> impl IntoView {
})>Click me</button>
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PostcardData {
name: String,
age: u32,
hobbies: Vec<String>,
}
/// This server function uses Postcard for both input and output encoding.
/// Postcard provides efficient binary serialization, almost as fast as rkyv, while also being
/// serde compatible
#[server(input = Postcard, output = Postcard)]
pub async fn postcard_example(
data: PostcardData,
) -> Result<PostcardData, ServerFnError> {
// Simulate some processing time
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
// Modify the data to demonstrate server-side changes
let mut modified_data = data.clone();
modified_data.age += 1;
modified_data.hobbies.push("Rust programming".to_string());
Ok(modified_data)
}
/// This component demonstrates the usage of Postcard encoding with server functions.
/// It allows incrementing the age of a person and shows how the data is
/// serialized, sent to the server, processed, and returned.
#[component]
pub fn PostcardExample() -> impl IntoView {
// Initialize the input data
let (input, set_input) = signal(PostcardData {
name: "Alice".to_string(),
age: 30,
hobbies: vec!["reading".to_string(), "hiking".to_string()],
});
// Create a resource that will call the server function whenever the input changes
let postcard_result = Resource::new(
move || input.get(),
|data| async move { postcard_example(data).await },
);
view! {
<h3>Using <code>postcard</code> encoding</h3>
<p>"This example demonstrates using Postcard for efficient binary serialization."</p>
<button on:click=move |_| {
// Update the input data when the button is clicked
set_input.update(|data| {
data.age += 1;
});
}>
"Increment Age"
</button>
// Display the current input data
<p>"Input: " {move || format!("{:?}", input.get())}</p>
<Transition>
// Display the result from the server, which will update automatically
// when the input changes due to the resource
<p>"Result: " {move || postcard_result.get().map(|r| format!("{:?}", r))}</p>
</Transition>
}
}

View File

@@ -22,7 +22,7 @@ leptos_router = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["actix"] }
serde_json = "1.0"
parking_lot = "0.12.3"
tracing = "0.1.40"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.39", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
@@ -31,3 +31,7 @@ rustdoc-args = ["--generate-link-to-definition"]
[features]
islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -207,7 +207,10 @@ impl ExtendResponse for ActixResponse {
/// without actually setting the status code. This means that the client will not follow the
/// redirect, and can therefore return the value of the server function and then handle
/// the redirect with client-side routing.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn redirect(path: &str) {
if let (Some(req), Some(res)) =
(use_context::<Request>(), use_context::<ResponseOptions>())
@@ -239,10 +242,14 @@ pub fn redirect(path: &str) {
);
}
} else {
tracing::warn!(
"Couldn't retrieve either Parts or ResponseOptions while trying \
to redirect()."
);
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect().";
#[cfg(feature = "tracing")]
tracing::warn!("{}", &msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", &msg);
}
}
@@ -282,7 +289,10 @@ pub fn redirect(path: &str) {
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|| {})
}
@@ -306,7 +316,10 @@ pub fn handle_server_fns() -> Route {
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn handle_server_fns_with_context(
additional_context: impl Fn() + 'static + Clone + Send,
) -> Route {
@@ -438,7 +451,10 @@ pub fn handle_server_fns_with_context(
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
@@ -505,7 +521,10 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
@@ -567,7 +586,10 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
@@ -590,7 +612,10 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -624,7 +649,10 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -655,7 +683,10 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -685,7 +716,10 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -704,7 +738,10 @@ where
})
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn provide_contexts(
req: Request,
meta_context: &ServerMetaContext,
@@ -1205,7 +1242,10 @@ where
InitError = (),
>,
{
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
@@ -1217,7 +1257,10 @@ where
self.leptos_routes_with_context(paths, || {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,
@@ -1304,7 +1347,10 @@ where
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl LeptosRoutes for &mut ServiceConfig {
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
@@ -1316,7 +1362,10 @@ impl LeptosRoutes for &mut ServiceConfig {
self.leptos_routes_with_context(paths, || {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,

View File

@@ -28,7 +28,7 @@ serde_json = "1.0"
tokio = { version = "1.39", default-features = false }
tower = "0.4.13"
tower-http = "0.5.2"
tracing = "0.1.40"
tracing = { version = "0.1.40", optional = true }
[dev-dependencies]
axum = "0.7.5"
@@ -38,6 +38,10 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs"]
islands-router = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -236,10 +236,14 @@ pub fn redirect(path: &str) {
);
}
} else {
tracing::warn!(
"Couldn't retrieve either Parts or ResponseOptions while trying \
to redirect()."
);
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect().";
#[cfg(feature = "tracing")]
tracing::warn!("{}", &msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", &msg);
}
}
@@ -289,7 +293,10 @@ pub fn generate_request_and_parts(
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub async fn handle_server_fns(req: Request<Body>) -> impl IntoResponse {
handle_server_fns_inner(|| {}, req).await
}
@@ -331,7 +338,10 @@ fn init_executor() {
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub async fn handle_server_fns_with_context(
additional_context: impl Fn() + 'static + Clone + Send,
req: Request<Body>,
@@ -460,7 +470,10 @@ pub type PinnedHtmlStream =
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
@@ -480,7 +493,10 @@ where
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
/// one respects the `SsrMode` on each Route and thus requires `Vec<AxumRouteListing>` for route checking.
/// This is useful if you are using `.leptos_routes_with_handler()`
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_route<IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -546,7 +562,10 @@ where
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
@@ -597,7 +616,10 @@ where
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -622,7 +644,10 @@ where
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
/// one respects the `SsrMode` on each Route, and thus requires `Vec<AxumRouteListing>` for route checking.
/// This is useful if you are using `.leptos_routes_with_handler()`.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_route_with_context<IV>(
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
@@ -701,7 +726,10 @@ where
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -766,7 +794,10 @@ where
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -859,7 +890,10 @@ where
})
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn provide_contexts(
path: &str,
meta_context: &ServerMetaContext,
@@ -924,7 +958,10 @@ fn provide_contexts(
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
@@ -976,7 +1013,10 @@ where
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -1041,7 +1081,10 @@ where
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
@@ -1072,7 +1115,10 @@ where
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> Vec<AxumRouteListing>
@@ -1085,7 +1131,10 @@ where
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<AxumRouteListing>, StaticDataMap)
@@ -1099,7 +1148,10 @@ where
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
@@ -1140,7 +1192,10 @@ pub async fn build_static_routes<IV>(
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
@@ -1227,7 +1282,10 @@ impl AxumRouteListing {
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
/// Additional context will be provided to the app Element.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
@@ -1554,7 +1612,10 @@ impl<S> LeptosRoutes<S> for axum::Router<S>
where
S: Clone + Send + Sync + 'static,
{
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes<IV>(
self,
state: &S,
@@ -1567,7 +1628,10 @@ where
self.leptos_routes_with_context(state, paths, || {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_context<IV>(
self,
state: &S,
@@ -1710,7 +1774,10 @@ where
router
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<AxumRouteListing>,

View File

@@ -16,7 +16,6 @@ leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
reactive_graph = { workspace = true, features = ["sandboxed-arenas"] }
tracing = "0.1.40"
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@@ -30,7 +30,7 @@ reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = "2.0"
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
thiserror = "1.0"
tracing = "0.1.40"
tracing = { version = "0.1.40", optional = true }
typed-builder = "0.19.1"
typed-builder-macro = "0.19.1"
serde = "1.0"
@@ -76,13 +76,20 @@ ssr = [
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
rkyv = ["server_fn/rkyv"]
tracing = [
"dep:tracing",
"reactive_graph/tracing",
"tachys/tracing",
] #, "leptos_macro/tracing", "leptos_dom/tracing"]
"leptos_macro/tracing",
"leptos_dom/tracing",
"leptos_server/tracing",
]
nonce = ["base64", "rand"]
spin = ["leptos-spin-macro"]
experimental-islands = ["leptos_macro/experimental-islands", "dep:serde_json"]
trace-component-props = ["leptos_macro/trace-component-props"]
trace-component-props = [
"leptos_macro/trace-component-props",
"leptos_dom/trace-component-props"
]
delegation = ["tachys/delegation"]
[package.metadata.cargo-all-features]
@@ -93,52 +100,22 @@ denylist = [
"rustls",
"default-tls",
"wasm-bindgen",
"rkyv", # was causing clippy issues on nightly
"rkyv", # was causing clippy issues on nightly
"trace-component-props",
"spin",
"experimental-islands",
]
skip_feature_sets = [
[
"csr",
"ssr",
],
[
"csr",
"hydrate",
],
[
"ssr",
"hydrate",
],
[
"serde",
"serde-lite",
],
[
"serde-lite",
"miniserde",
],
[
"serde",
"miniserde",
],
[
"serde",
"rkyv",
],
[
"miniserde",
"rkyv",
],
[
"serde-lite",
"rkyv",
],
[
"default-tls",
"rustls",
],
["csr", "ssr"],
["csr", "hydrate"],
["ssr", "hydrate"],
["serde", "serde-lite"],
["serde-lite", "miniserde"],
["serde", "miniserde"],
["serde", "rkyv"],
["miniserde", "rkyv"],
["serde-lite", "rkyv"],
["default-tls", "rustls"],
]
[package.metadata.docs.rs]

View File

@@ -43,10 +43,7 @@ use leptos_reactive::{
/// }
/// # }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all)
)]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn AnimatedShow(
/// The components Show wraps

View File

@@ -10,13 +10,13 @@
//!
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<i32, String>,
//! #[prop(into)] render_number: Callback<(i32,), String>,
//! ) -> impl IntoView {
//! view! {
//! <div>
//! {render_number.run(1)}
//! {render_number.run((1,))}
//! // callbacks can be called multiple times
//! {render_number.run(42)}
//! {render_number.run((42,))}
//! </div>
//! }
//! }
@@ -85,17 +85,39 @@ impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
}
}
impl<F, In, T, Out> From<F> for UnsyncCallback<In, Out>
where
F: Fn(In) -> T + 'static,
T: Into<Out> + 'static,
In: 'static,
{
fn from(f: F) -> Self {
Self::new(move |x| f(x).into())
}
macro_rules! impl_unsync_callable_from_fn {
($($arg:ident),*) => {
impl<F, $($arg,)* T, Out> From<F> for UnsyncCallback<($($arg,)*), Out>
where
F: Fn($($arg),*) -> T + 'static,
T: Into<Out> + 'static,
$($arg: 'static,)*
{
fn from(f: F) -> Self {
paste::paste!(
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
)
}
}
};
}
impl_unsync_callable_from_fn!();
impl_unsync_callable_from_fn!(P1);
impl_unsync_callable_from_fn!(P1, P2);
impl_unsync_callable_from_fn!(P1, P2, P3);
impl_unsync_callable_from_fn!(P1, P2, P3, P4);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
impl_unsync_callable_from_fn!(
P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12
);
/// Callbacks define a standard way to store functions and closures.
///
/// # Example
@@ -104,11 +126,11 @@ where
/// # use leptos::callback::{Callable, Callback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: Callback<i32, String>,
/// #[prop(into)] render_number: Callback<(i32,), String>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.run(42)}
/// {render_number.run((42,))}
/// </div>
/// }
/// }
@@ -148,17 +170,37 @@ impl<In, Out> Clone for Callback<In, Out> {
impl<In, Out> Copy for Callback<In, Out> {}
impl<F, In, T, Out> From<F> for Callback<In, Out>
where
F: Fn(In) -> T + Send + Sync + 'static,
T: Into<Out> + 'static,
In: Send + Sync + 'static,
{
fn from(f: F) -> Self {
Self::new(move |x| f(x).into())
}
macro_rules! impl_callable_from_fn {
($($arg:ident),*) => {
impl<F, $($arg,)* T, Out> From<F> for Callback<($($arg,)*), Out>
where
F: Fn($($arg),*) -> T + Send + Sync + 'static,
T: Into<Out> + 'static,
$($arg: Send + Sync + 'static,)*
{
fn from(f: F) -> Self {
paste::paste!(
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
)
}
}
};
}
impl_callable_from_fn!();
impl_callable_from_fn!(P1);
impl_callable_from_fn!(P1, P2);
impl_callable_from_fn!(P1, P2, P3);
impl_callable_from_fn!(P1, P2, P3, P4);
impl_callable_from_fn!(P1, P2, P3, P4, P5);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12);
impl<In: 'static, Out: 'static> Callback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(fun: F) -> Self
@@ -190,11 +232,15 @@ mod tests {
#[test]
fn runback_from() {
let _callback: Callback<(), String> = (|()| "test").into();
let _callback: Callback<(), String> = (|| "test").into();
let _callback: Callback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
}
#[test]
fn sync_callback_from() {
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
let _callback: UnsyncCallback<(), String> = (|| "test").into();
let _callback: UnsyncCallback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
}
}

View File

@@ -69,10 +69,7 @@ use web_sys::{
/// Ok(())
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn>(
/// The action from which to build the form. This should include a URL, which can be generated

View File

@@ -302,6 +302,9 @@ pub use serde;
#[cfg(feature = "experimental-islands")]
#[doc(hidden)]
pub use serde_json;
#[cfg(feature = "tracing")]
#[doc(hidden)]
pub use tracing;
#[doc(hidden)]
pub use wasm_bindgen;
#[doc(hidden)]
@@ -376,7 +379,7 @@ pub use show::*;
//pub use suspense_component::*;
mod suspense_component;
//mod transition;
#[cfg(any(debug_assertions, feature = "ssr"))]
#[cfg(feature = "tracing")]
#[doc(hidden)]
pub use tracing;
pub use transition::*;

View File

@@ -10,10 +10,7 @@ use std::sync::Arc;
/// If no mount point is given, the portal is inserted in `document.body`;
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all)
)]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn Portal<V>(
/// Target element where the children will be appended

View File

@@ -13,7 +13,7 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Get, Read, Track, With},
traits::{Dispose, Get, Read, Track, With},
};
use slotmap::{DefaultKey, SlotMap};
use tachys::{
@@ -277,7 +277,7 @@ where
futures::channel::oneshot::channel::<()>();
let mut tasks_tx = Some(tasks_tx);
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
let eff = reactive_graph::effect::Effect::new_isomorphic({
move |_| {
tasks.track();
if tasks.read().is_empty() {
@@ -337,7 +337,7 @@ where
}
children = children => {
// clean up the (now useless) effect
drop(eff);
eff.dispose();
Some(OwnedView::new_with_owner(children, owner))
}

View File

@@ -14,8 +14,10 @@ reactive_graph = { workspace = true }
or_poisoned = { workspace = true }
js-sys = "0.3.69"
send_wrapper = "0.6.0"
tracing = "0.1.40"
tracing = { version = "0.1.40", optional = true }
wasm-bindgen = "0.2.93"
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", optional = true }
[dev-dependencies]
leptos = { path = "../leptos" }
@@ -26,7 +28,11 @@ features = ["Location"]
[features]
default = []
tracing = []
tracing = ["dep:tracing"]
trace-component-props = ["dep:serde", "dep:serde_json"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -4,6 +4,8 @@
//! DOM helpers for Leptos.
pub mod helpers;
#[doc(hidden)]
pub mod macro_helpers;
/// Utilities for simple isomorphic logging to the console or terminal.
#[macro_use]

View File

@@ -0,0 +1,3 @@
#[cfg(feature = "trace-component-props")]
#[doc(hidden)]
pub mod tracing_property;

View File

@@ -0,0 +1,170 @@
use wasm_bindgen::UnwrapThrowExt;
#[macro_export]
/// Use for tracing property
macro_rules! tracing_props {
() => {
::leptos::tracing::span!(
::leptos::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props = String::from("[]")
);
};
($($prop:tt),+ $(,)?) => {
{
use ::leptos::leptos_dom::macro_helpers::tracing_property::{Match, SerializeMatch, DefaultMatch};
let mut props = String::from('[');
$(
let prop = (&&Match {
name: stringify!{$prop},
value: std::cell::Cell::new(Some(&$prop))
}).spez();
props.push_str(&format!("{prop},"));
)*
props.pop();
props.push(']');
::leptos::tracing::span!(
::leptos::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props
);
}
};
}
// Implementation based on spez
// see https://github.com/m-ou-se/spez
pub struct Match<T> {
pub name: &'static str,
pub value: std::cell::Cell<Option<T>>,
}
pub trait SerializeMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
// suppresses warnings when serializing signals into props
#[cfg(debug_assertions)]
let _z = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
serde_json::to_string(self.value.get().unwrap_throw()).map_or_else(
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
)
}
}
pub trait DefaultMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T> DefaultMatch for Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
}
}
#[test]
fn match_primitive() {
// String
let test = String::from("string");
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// &str
let test = "string";
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// u128
let test: u128 = 1;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": 1}"#);
// i128
let test: i128 = -1;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": -1}"#);
// f64
let test = 3.25;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": 3.25}"#);
// bool
let test = true;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": true}"#);
}
#[test]
fn match_serialize() {
use serde::Serialize;
#[derive(Serialize)]
struct CustomStruct {
field: &'static str,
}
let test = CustomStruct { field: "field" };
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
// Verification of ownership
assert_eq!(test.field, "field");
}
#[test]
#[allow(clippy::needless_borrow)]
fn match_no_serialize() {
struct CustomStruct {
field: &'static str,
}
let test = CustomStruct { field: "field" };
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(
prop,
r#"{"name": "test", "value": "[unserializable value]"}"#
);
// Verification of ownership
assert_eq!(test.field, "field");
}

View File

@@ -27,7 +27,7 @@ leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"
uuid = { version = "1.10", features = ["v4"] }
tracing = "0.1.40"
tracing = { version = "0.1.40", optional = true }
[dev-dependencies]
log = "0.4.22"
@@ -43,7 +43,7 @@ csr = []
hydrate = []
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = []
tracing = ["dep:tracing"]
experimental-islands = []
trace-component-props = []
actix = ["server_fn_macro/actix"]

View File

@@ -142,6 +142,7 @@ impl ToTokens for Model {
let props_name = format_ident!("{name}Props");
let props_builder_name = format_ident!("{name}PropsBuilder");
let props_serialized_name = format_ident!("{name}PropsSerialized");
#[cfg(feature = "tracing")]
let trace_name = format!("<{name} />");
let is_island_with_children = *is_island
@@ -190,32 +191,38 @@ impl ToTokens for Model {
tracing_span_expr,
tracing_guard_expr,
tracing_props_expr,
) = if cfg!(feature = "tracing") {
(
quote! {
#[allow(clippy::let_with_type_underscore)]
#[cfg_attr(
any(debug_assertions, feature="ssr"),
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
)]
},
quote! {
let span = ::leptos::tracing::Span::current();
},
quote! {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
if no_props {
quote! {}
} else {
) = {
#[cfg(feature = "tracing")]
{
(
quote! {
::leptos::tracing_props![#prop_names];
}
},
)
} else {
(quote! {}, quote! {}, quote! {}, quote! {})
#[allow(clippy::let_with_type_underscore)]
#[cfg_attr(
feature = "tracing",
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
)]
},
quote! {
let span = ::leptos::tracing::Span::current();
},
quote! {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
if no_props || !cfg!(feature = "trace-component-props") {
quote!()
} else {
quote! {
::leptos::leptos_dom::tracing_props![#prop_names];
}
},
)
}
#[cfg(not(feature = "tracing"))]
{
(quote!(), quote!(), quote!(), quote!())
}
};
let component_id = name.to_string();

View File

@@ -13,6 +13,7 @@ use component::DummyModel;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use quote::{quote, ToTokens};
use std::str::FromStr;
use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility};
mod params;
@@ -260,10 +261,7 @@ mod slot;
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro]
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn view(tokens: TokenStream) -> TokenStream {
let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter();
@@ -331,6 +329,31 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
}
}
/// This behaves like the [`view`] macro, but loads the view from an external file instead of
/// parsing it inline.
///
/// This is designed to allow editing views in a separate file, if this improves a user's workflow.
///
/// The file is loaded and parsed during proc-macro execution, and its path is resolved relative to
/// the crate root rather than relative to the file from which it is called.
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn include_view(tokens: TokenStream) -> TokenStream {
let file_name = syn::parse::<syn::LitStr>(tokens).unwrap_or_else(|_| {
abort!(
Span::call_site(),
"the only supported argument is a string literal"
);
});
let file =
std::fs::read_to_string(file_name.value()).unwrap_or_else(|_| {
abort!(Span::call_site(), "could not open file");
});
let tokens = proc_macro2::TokenStream::from_str(&file)
.unwrap_or_else(|e| abort!(Span::call_site(), e));
view(tokens.into())
}
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
///
/// The `#[component]` macro allows you to annotate plain Rust functions as components

View File

@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
base64 = "0.22.1"
codee = { version = "0.1.2", features = ["json_serde"] }
codee = { version = "0.2.0", features = ["json_serde"] }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }
@@ -38,7 +38,7 @@ tachys = ["dep:tachys"]
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]
denylist = ["nightly"]
denylist = ["tracing"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@@ -95,8 +95,8 @@ where
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn dispatch(&self, input: I) {
self.0.with_value(|a| a.dispatch(input))
@@ -104,8 +104,8 @@ where
/// The set of all submissions to this multi-action.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.0.with_value(|a| a.submissions())
@@ -119,8 +119,8 @@ where
/// How many times an action has successfully resolved.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn version(&self) -> RwSignal<usize> {
self.0.with_value(|a| a.version)
@@ -129,8 +129,8 @@ where
/// Associates the URL of the given server function with this action.
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn using_server_fn<T: ServerFn>(self) -> Self {
self.0.update_value(|a| {
@@ -201,8 +201,8 @@ where
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn dispatch(&self, input: I) {
if !is_suppressing_resource_load() {
@@ -293,10 +293,7 @@ where
/// create_multi_action(|input: &(usize, String)| async { todo!() });
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn create_multi_action<I, O, F, Fu>(action_fn: F) -> MultiAction<I, O>
where
I: 'static,
@@ -333,10 +330,7 @@ where
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>();
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn create_server_multi_action<S>(
) -> MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
where

View File

@@ -129,6 +129,14 @@ where
}
}
#[track_caller]
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
where
T: Send + Sync + 'static,
{
self.data.try_with(|n| n.as_ref().map(f))?
}
/// Re-runs the async function with the current source data.
pub fn refetch(&self) {
*self.refetch.write() += 1;
@@ -172,6 +180,24 @@ where
}
}
impl<T, E, Ser> ArcResource<Result<T, E>, Ser>
where
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
<Ser as Encoder<Result<T, E>>>::Error: Debug,
<Ser as Decoder<Result<T, E>>>::Error: Debug,
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
T: Send + Sync + 'static,
E: Send + Sync + Clone + 'static,
{
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T> ArcResource<T, JsonSerdeCodec>
where
JsonSerdeCodec: Encoder<T> + Decoder<T>,
@@ -675,12 +701,36 @@ where
}
}
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
self.data
.try_with(|n| n.as_ref().map(|n| Some(f(n))))?
.flatten()
}
/// Re-runs the async function with the current source data.
pub fn refetch(&self) {
self.refetch.try_update(|n| *n += 1);
}
}
impl<T, E, Ser> Resource<Result<T, E>, Ser>
where
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
<Ser as Encoder<Result<T, E>>>::Error: Debug,
<Ser as Decoder<Result<T, E>>>::Error: Debug,
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
T: Send + Sync,
E: Send + Sync + Clone,
{
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T, Ser> IntoFuture for Resource<T, Ser>
where
T: Clone + Send + Sync + 'static,

View File

@@ -14,7 +14,7 @@ once_cell = "1.19"
or_poisoned = { workspace = true }
indexmap = "2.3"
send_wrapper = "0.6.0"
tracing = "0.1.40"
tracing = { version = "0.1.40", optional = true }
wasm-bindgen = "0.2.93"
futures = "0.3.30"
@@ -25,6 +25,10 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
[features]
default = []
ssr = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -342,10 +342,14 @@ where
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
tracing::warn!(
"tried to use a leptos_meta component without `ServerMetaContext` \
provided"
);
let msg = "tried to use a leptos_meta component without \
`ServerMetaContext` provided";
#[cfg(feature = "tracing")]
tracing::warn!("{}", msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", msg);
}
RegisteredMetaTag { el }

View File

@@ -44,3 +44,6 @@ sandboxed-arenas = []
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -163,10 +163,10 @@ where
#[deprecated = "This function is being removed to conform to Rust idioms. \
Please use `Selector::new()` instead."]
pub fn create_selector<T>(
source: impl Fn() -> T + Clone + 'static,
source: impl Fn() -> T + Clone + Send + Sync + 'static,
) -> Selector<T>
where
T: PartialEq + Eq + Clone + std::hash::Hash + 'static,
T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static,
{
Selector::new(source)
}
@@ -178,11 +178,11 @@ where
#[deprecated = "This function is being removed to conform to Rust idioms. \
Please use `Selector::new_with_fn()` instead."]
pub fn create_selector_with_fn<T>(
source: impl Fn() -> T + Clone + 'static,
source: impl Fn() -> T + Clone + Send + Sync + 'static,
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Selector<T>
where
T: PartialEq + Eq + Clone + std::hash::Hash + 'static,
T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static,
{
Selector::new_with_fn(source, f)
}

View File

@@ -109,7 +109,7 @@ where
#[track_caller]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
pub fn new(fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static) -> Self
where
@@ -127,7 +127,7 @@ where
#[track_caller]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
pub fn new_with_compare(
fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static,
@@ -153,7 +153,7 @@ where
#[track_caller]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
pub fn new_owning(
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,

View File

@@ -149,7 +149,7 @@ where
#[track_caller]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "debug", skip_all,)
tracing::instrument(level = "debug", skip_all)
)]
/// Creates a new memoized, computed reactive value.
///
@@ -184,7 +184,7 @@ where
#[track_caller]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
/// Creates a new memo with a custom comparison function. By default, memos simply use
/// [`PartialEq`] to compare the previous value to the new value. Passing a custom comparator
@@ -218,7 +218,7 @@ where
#[track_caller]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
pub fn new_owning(
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,

View File

@@ -30,7 +30,7 @@ use std::{
/// let a = RwSignal::new(0);
/// let is_selected = Selector::new(move || a.get());
/// let total_notifications = StoredValue::new(0);
/// Effect::new({
/// Effect::new_isomorphic({
/// let is_selected = is_selected.clone();
/// move |_| {
/// if is_selected.selected(5) {
@@ -55,7 +55,7 @@ use std::{
///
/// # any_spawner::Executor::tick().await;
/// assert_eq!(is_selected.selected(5), false);
/// # });
/// # }).await;
/// # });
/// ```
#[derive(Clone)]
@@ -74,17 +74,17 @@ where
impl<T> Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
T: PartialEq + Send + Sync + Eq + Clone + Hash + 'static,
{
/// Creates a new selector that compares values using [`PartialEq`].
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
pub fn new(source: impl Fn() -> T + Send + Sync + Clone + 'static) -> Self {
Self::new_with_fn(source, PartialEq::eq)
}
/// Creates a new selector that compares values by returning `true` from a comparator function
/// if the values are the same.
pub fn new_with_fn(
source: impl Fn() -> T + Clone + 'static,
source: impl Fn() -> T + Clone + Send + Sync + 'static,
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Self {
let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
@@ -92,7 +92,7 @@ where
let v: Arc<RwLock<Option<T>>> = Default::default();
let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
let effect = Arc::new(RenderEffect::new({
let effect = Arc::new(RenderEffect::new_isomorphic({
let subs = Arc::clone(&subs);
let f = Arc::clone(&f);
let v = Arc::clone(&v);

View File

@@ -43,6 +43,7 @@ use std::{
/// # use reactive_graph::owner::StoredValue;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// let a = RwSignal::new(0);
/// let b = RwSignal::new(0);
///
@@ -52,7 +53,9 @@ use std::{
/// println!("Value: {}", a.get());
/// });
///
/// # assert_eq!(a.get(), 0);
/// a.set(1);
/// # assert_eq!(a.get(), 1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
@@ -61,7 +64,7 @@ use std::{
/// // and easily lead to problems like infinite loops
/// b.set(a.get() + 1);
/// });
/// # });
/// # }).await;
/// # });
/// ```
/// ## Web-Specific Notes
@@ -182,6 +185,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
///
@@ -192,13 +196,16 @@ impl Effect<LocalStorage> {
/// },
/// false,
/// );
/// # assert_eq!(num.get(), 0);
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # assert_eq!(num.get(), 1);
///
/// effect.stop(); // stop watching
///
/// set_num.set(2); // (nothing happens)
/// # });
/// # assert_eq!(num.get(), 2);
/// # }).await;
/// # });
/// ```
///
@@ -210,6 +217,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
/// let (cb_num, set_cb_num) = signal(0);
@@ -222,12 +230,17 @@ impl Effect<LocalStorage> {
/// false,
/// );
///
/// # assert_eq!(num.get(), 0);
/// set_num.set(1); // > "Number: 1; Cb: 0"
/// # assert_eq!(num.get(), 1);
///
/// # assert_eq!(cb_num.get(), 0);
/// set_cb_num.set(1); // (nothing happens)
/// # assert_eq!(cb_num.get(), 1);
///
/// set_num.set(2); // > "Number: 2; Cb: 1"
/// # });
/// # assert_eq!(num.get(), 2);
/// # }).await;
/// # });
/// ```
///
@@ -243,6 +256,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
///
@@ -254,8 +268,10 @@ impl Effect<LocalStorage> {
/// true,
/// ); // > "Number: 0; Prev: None"
///
/// # assert_eq!(num.get(), 0);
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # });
/// # assert_eq!(num.get(), 1);
/// # }).await;
/// # });
/// ```
pub fn watch<D, T>(

View File

@@ -135,44 +135,50 @@ where
{
/// Creates a render effect that will run whether the `effects` feature is enabled or not.
pub fn new_isomorphic(
mut fun: impl FnMut(Option<T>) -> T + Send + 'static,
fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static,
) -> Self {
let (mut observer, mut rx) = channel();
observer.notify();
fn erased<T: Send + Sync + 'static>(
mut fun: Box<dyn FnMut(Option<T>) -> T + Send + Sync + 'static>,
) -> RenderEffect<T> {
let (observer, mut rx) = channel();
let value = Arc::new(RwLock::new(None::<T>));
let owner = Owner::new();
let inner = Arc::new(RwLock::new(EffectInner {
dirty: false,
observer,
sources: SourceSet::new(),
}));
let value = Arc::new(RwLock::new(None::<T>));
let owner = Owner::new();
let inner = Arc::new(RwLock::new(EffectInner {
dirty: false,
observer,
sources: SourceSet::new(),
}));
let mut first_run = true;
let initial_value = owner
.with(|| inner.to_any_subscriber().with_observer(|| fun(None)));
*value.write().or_poisoned() = Some(initial_value);
Executor::spawn({
let value = Arc::clone(&value);
let subscriber = inner.to_any_subscriber();
Executor::spawn({
let value = Arc::clone(&value);
let subscriber = inner.to_any_subscriber();
async move {
while rx.next().await.is_some() {
if first_run
|| subscriber
async move {
while rx.next().await.is_some() {
if subscriber
.with_observer(|| subscriber.update_if_necessary())
{
first_run = false;
subscriber.clear_sources(&subscriber);
{
subscriber.clear_sources(&subscriber);
let old_value =
mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| fun(old_value))
});
*value.write().or_poisoned() = Some(new_value);
let old_value =
mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| fun(old_value))
});
*value.write().or_poisoned() = Some(new_value);
}
}
}
}
});
RenderEffect { value, inner }
});
RenderEffect { value, inner }
}
erased(Box::new(fun))
}
}

View File

@@ -149,7 +149,7 @@ impl<T> ArcRwSignal<T> {
/// Creates a new signal, taking the initial value as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
#[track_caller]
pub fn new(value: T) -> Self {

View File

@@ -118,7 +118,7 @@ where
/// Creates a new signal, taking the initial value as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
#[track_caller]
pub fn new(value: T) -> Self {
@@ -134,7 +134,7 @@ where
/// Creates a new signal with the given arena storage method.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
#[track_caller]
pub fn new_with_storage(value: T) -> Self {
@@ -154,7 +154,7 @@ where
/// this pins the value to the current thread. Accessing it from any other thread will panic.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
tracing::instrument(level = "trace", skip_all)
)]
#[track_caller]
pub fn new_local(value: T) -> Self {

View File

@@ -316,7 +316,7 @@ mod tests {
assert_eq!(store.user().read_untracked().as_str(), "Bob");
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev| {
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
@@ -360,7 +360,7 @@ mod tests {
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev| {
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
@@ -392,7 +392,7 @@ mod tests {
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev| {
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
@@ -429,7 +429,7 @@ mod tests {
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev| {
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {

View File

@@ -64,3 +64,6 @@ nightly = []
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -47,6 +47,7 @@ serde-lite = { version = "0.5.0", features = ["derive"], optional = true }
futures = "0.3.30"
http = { version = "1.1" }
ciborium = { version = "0.2.2", optional = true }
postcard = { version = "1", features = ["alloc"], optional = true }
hyper = { version = "1.4", optional = true }
bytes = "1.7"
http-body-util = { version = "0.1.2", optional = true }
@@ -108,6 +109,7 @@ url = ["dep:serde_qs"]
cbor = ["dep:ciborium"]
rkyv = ["dep:rkyv"]
msgpack = ["dep:rmp-serde"]
postcard = ["dep:postcard"]
default-tls = ["reqwest?/default-tls"]
rustls = ["reqwest?/rustls-tls"]
reqwest = ["dep:reqwest"]
@@ -196,4 +198,24 @@ skip_feature_sets = [
"url",
"serde-lite",
],
[
"postcard",
"json",
],
[
"postcard",
"cbor",
],
[
"postcard",
"url",
],
[
"postcard",
"serde-lite",
],
[
"postcard",
"rkyv",
],
]

View File

@@ -49,6 +49,11 @@ mod msgpack;
#[cfg(feature = "msgpack")]
pub use msgpack::*;
#[cfg(feature = "postcard")]
mod postcard;
#[cfg(feature = "postcard")]
pub use postcard::*;
mod stream;
use crate::error::ServerFnError;
use futures::Future;

View File

@@ -0,0 +1,74 @@
use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
use crate::{
error::ServerFnError,
request::{ClientReq, Req},
response::{ClientRes, Res},
};
use bytes::Bytes;
use http::Method;
use serde::{de::DeserializeOwned, Serialize};
/// A codec for Postcard.
pub struct Postcard;
impl Encoding for Postcard {
const CONTENT_TYPE: &'static str = "application/x-postcard";
const METHOD: Method = Method::POST;
}
impl<T, Request, Err> IntoReq<Postcard, Request, Err> for T
where
Request: ClientReq<Err>,
T: Serialize,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = postcard::to_allocvec(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post_bytes(
path,
Postcard::CONTENT_TYPE,
accepts,
Bytes::from(data),
)
}
}
impl<T, Request, Err> FromReq<Postcard, Request, Err> for T
where
Request: Req<Err> + Send,
T: DeserializeOwned,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
let data = req.try_into_bytes().await?;
postcard::from_bytes::<T>(&data)
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<T, Response, Err> IntoRes<Postcard, Response, Err> for T
where
Response: Res<Err>,
T: Serialize + Send,
{
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = postcard::to_allocvec(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_bytes(Postcard::CONTENT_TYPE, Bytes::from(data))
}
}
impl<T, Response, Err> FromRes<Postcard, Response, Err> for T
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
let data = res.try_into_bytes().await?;
postcard::from_bytes(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}

View File

@@ -178,3 +178,6 @@ testing = ["dep:slotmap"]
reactive_graph = ["dep:reactive_graph", "dep:any_spawner"]
sledgehammer = ["dep:sledgehammer_bindgen", "dep:sledgehammer_utils"]
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -258,7 +258,7 @@ html_self_closing_elements! {
/// The `<meta>` HTML element represents Metadata that cannot be represented by other HTML meta-related elements, like base, link, script, style or title.
meta HtmlMetaElement [charset, content, http_equiv, name] true,
/// The `<source>` HTML element specifies multiple media resources for the picture, the audio element, or the video element. It is an empty element, meaning that it has no content and does not have a closing tag. It is commonly used to offer the same media content in multiple file formats in order to provide compatibility with a broad range of browsers given their differing support for image file formats and media file formats.
source HtmlSourceElement [src, r#type] true,
source HtmlSourceElement [src, r#type, srcset, sizes, media, height, width] true,
/// The `<track>` HTML element is used as a child of the media elements, audio and video. It lets you specify timed text tracks (or time-based data), for example to automatically handle subtitles. The tracks are formatted in WebVTT format (.vtt files) — Web Video Text Tracks.
track HtmlTrackElement [default, kind, label, src, srclang] true,
/// The `<wbr>` HTML element represents a word break opportunity—a position within text where the browser may optionally break a line, though its line-breaking rules would not otherwise create a break at that location.

View File

@@ -562,7 +562,7 @@ mod tests {
element::{em, ElementChild, Main},
},
renderer::mock_dom::MockDom,
view::{Render, RenderHtml},
view::Render,
};
#[test]