Compare commits

..

109 Commits

Author SHA1 Message Date
pengx17
f544e69d02 fix(mobile): modal styles on mobile (#8023)
<div class='graphite__hidden'>
          <div>🎥 Video uploaded on Graphite:</div>
            <a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/92c0cc55-1f75-42b7-a83f-4ffa6cf205ad.mp4">
              <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/92c0cc55-1f75-42b7-a83f-4ffa6cf205ad.mp4">
            </a>
          </div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/92c0cc55-1f75-42b7-a83f-4ffa6cf205ad.mp4">20240829-1420-07.7370936.mp4</video>
2024-08-29 16:45:23 +00:00
pengx17
adf314d594 fix(mobile): adjust peek view style for mobile (#8003)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/c3a04128-0309-4df6-ab6d-0a5129483c73.png)
2024-08-29 16:45:22 +00:00
pengx17
7ae141bd9e feat(mobile): add mobile detail page (#7993)
fix AF-1241
2024-08-29 16:45:22 +00:00
pengx17
f8e6f1f2b5 chore: add mobile scope (#8020) 2024-08-29 16:45:21 +00:00
CatsJuice
f1bb1fc9b8 feat(mobile): search page ui (#8012)
feat(mobile): search page ui

fix(core): quick search tags performance issue
2024-08-29 09:05:23 +00:00
JimmFly
5e8683c9be feat(core): add outgoing links to doc info (#7955)
close AF-1270

![CleanShot 2024-08-23 at 13 22 38@2x](https://github.com/user-attachments/assets/7eb21db5-ab33-41ad-a51a-fd2cf46a0e30)
2024-08-29 06:41:41 +00:00
CatsJuice
3ce92f2abc feat(mobile): all docs page ui impl (#7976) 2024-08-29 06:09:48 +00:00
CatsJuice
db76780bc9 feat(mobile): mobile index page UI (#7959) 2024-08-29 06:09:47 +00:00
CatsJuice
f37051dc87 feat(core): mobile renderer for explorer (#7942) 2024-08-29 06:09:45 +00:00
EYHN
b96ad57568 feat(core): import template (#8000) 2024-08-29 04:01:35 +00:00
JimmFly
4ec45a247e feat(core): add sign in button to shared doc (#7952)
![CleanShot 2024-08-23 at 12 05 12@2x](https://github.com/user-attachments/assets/2c146707-9551-4044-b289-0904653f30f2)
2024-08-29 02:44:06 +00:00
darkskygit
dde45748d9 feat: filter out empty workpace in sidebar list (#7960)
fix PD-1567
2024-08-28 08:43:44 +00:00
L-Sun
06685683ae fix(core): add mobile edit button (#7996) 2024-08-28 06:38:45 +00:00
JimmFly
65a87196d5 feat(core): impl the Doc Info and Bi-Directional Links display settings (#7991)
https://github.com/user-attachments/assets/a469254c-a2ea-4cf4-837e-f9a8bbe5b378
2024-08-28 02:35:29 +00:00
JimmFly
09ab922572 feat(core): add new doc default mode setting (#7990)
https://github.com/user-attachments/assets/523b14f3-ee42-4061-8ca2-221e071d5cc9
2024-08-28 02:35:28 +00:00
JimmFly
c53adbc7e8 chore: adjust font menu and slider style (#7989)
![CleanShot 2024-08-27 at 15 52 09@2x](https://github.com/user-attachments/assets/69c8a340-0d59-4d8d-846e-73dc81f2800a)
![CleanShot 2024-08-27 at 15 52 39@2x](https://github.com/user-attachments/assets/2177f267-bdc1-459b-b043-868642b08d9a)
2024-08-28 02:35:26 +00:00
JimmFly
03b2cda845 refactor(core): move fontFamily and fullWidthLayout to editor settings (#7988) 2024-08-28 02:35:24 +00:00
EYHN
3e810eb043 fix(core): no share page in desktop (#7983) 2024-08-27 12:37:30 +00:00
JimmFly
b8f07ce3fc chore(core): disable expand database block (#7984)
close AF-1271
2024-08-27 12:24:48 +00:00
darkskygit
10a066a52a fix(native): return type casts (#7986) 2024-08-27 05:53:05 +00:00
DarkSky
6557b5d4b6 fix: native binding (#7985) 2024-08-27 12:31:25 +08:00
Brooooooklyn
67c9c7bcc5 ci: send slack message to channel after deploy (#7889) 2024-08-27 03:01:55 +00:00
renovate
e44a9483c2 chore: bump up @marsidev/react-turnstile version to v1 (#7940)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@marsidev/react-turnstile](https://togithub.com/marsidev/react-turnstile) | [`^0.7.0` -> `^1.0.0`](https://renovatebot.com/diffs/npm/@marsidev%2freact-turnstile/0.7.2/1.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@marsidev%2freact-turnstile/1.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@marsidev%2freact-turnstile/1.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@marsidev%2freact-turnstile/0.7.2/1.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@marsidev%2freact-turnstile/0.7.2/1.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>marsidev/react-turnstile (@&#8203;marsidev/react-turnstile)</summary>

### [`v1.0.0`](https://togithub.com/marsidev/react-turnstile/releases/tag/v1.0.0)

[Compare Source](https://togithub.com/marsidev/react-turnstile/compare/v0.7.2...v1.0.0)

##### 🚨 Breaking Changes

-   Now we ship ESM-only code, this will decrease the bundle size significantly.
    If you are a CommonJS user, I suggest you to keep using the version `0.7.2` or upgrade your project to ESM.

-   Removed "auto" widget size due to is not even a valid size for Cloudflare Turnstile.
    We have been sending `undefined` when the size was "auto", so if you were using this size, then you can simply remove it.

-   Peer dependencies for `react` and `react-dom` have been updated to 17.x and above.

##### 🚀 Features

-   Added callback `onTimeout`
-   Added render options `refreshTimeout` and `feedbackEnabled` (Closes [#&#8203;82](https://togithub.com/marsidev/react-turnstile/issues/82))
-   Added new "flexible" widget size (Closes [#&#8203;82](https://togithub.com/marsidev/react-turnstile/issues/82))

##### 🐛 Fixes

-   Updated "compact" widget size to 150x140 (Closes [#&#8203;85](https://togithub.com/marsidev/react-turnstile/issues/85))

##### 🛠 Misc

##### Build

-   We migrated from [unbuild](https://togithub.com/unjs/unbuild) to [tsup](https://togithub.com/egoist/tsup)
-   The generated build is now minified, this will decrease the bundle size significantly.

##### TypeScript

-   Improved type definitions. Now we expose these types: `AppearanceMode`, `ExecutionMode`, `FailureRetryMode`, `RefreshExpiredMode`, `RefreshTimeoutMode`, `WidgetSize`. Inspired by [@&#8203;types/cloudflare-turnstile](https://www.npmjs.com/package/@&#8203;types/cloudflare-turnstile)

##### Other

-   Updated dependencies
-   Updated docs
-   Added issue templates and policies

#####     [View changes on GitHub](https://togithub.com/marsidev/react-turnstile/compare/v0.7.2...v1.0.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-27 02:50:53 +00:00
renovate
ecf50a4dad chore: bump up happy-dom version to v15 (#7941)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [happy-dom](https://togithub.com/capricorn86/happy-dom) | [`^14.7.1` -> `^15.0.0`](https://renovatebot.com/diffs/npm/happy-dom/14.12.3/15.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/15.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/happy-dom/15.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/happy-dom/14.12.3/15.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/14.12.3/15.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>capricorn86/happy-dom (happy-dom)</summary>

### [`v15.0.0`](https://togithub.com/capricorn86/happy-dom/releases/tag/v15.0.0)

[Compare Source](https://togithub.com/capricorn86/happy-dom/compare/v14.12.3...v15.0.0)

##### 💣 Breaking Changes

-   Implements remaining HTML elements - By **[@&#8203;betterqualityassuranceuser](https://togithub.com/betterqualityassuranceuser)** in task [#&#8203;1332](https://togithub.com/capricorn86/happy-dom/issues/1332)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-27 02:40:19 +00:00
forehalo
0209e3fa76 fix(core): avoid expand runtime config everywhere used (#7972) 2024-08-26 12:22:52 +00:00
EYHN
9ea4aaaf37 refactor(infra): remove setimmediate (#7975) 2024-08-26 11:57:24 +00:00
pengx17
611925fa10 fix(electron): adjust app-tabs-header styles (#7961)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/6dc54c24-e90f-4c3f-a743-ce7f4512e616.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/3cbfe8f8-d4b8-4167-b557-89184bdcce9c.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/28ba3ad1-22a7-487a-a9a4-d53a658457f9.png)

also fix PD-1631
2024-08-26 09:34:27 +00:00
EYHN
bc86f0a672 feat(core): editor setting service (#7956)
define editor setting schema in `packages/frontend/core/src/modules/editor-settting/schema.ts`

e.g.

```ts
const BSEditorSettingSchema = z.object({
  connector: z.object({
    stroke: z
      .union([
        z.string(),
        z.object({
          dark: z.string(),
          light: z.string(),
        }),
      ])
      .default('#000000'), // default is necessary
  }),
});
```

schema can be defined in a nested way. EditorSetting api is in flat way:

editorSetting api:

```ts
editorSetting.settings$ === {
  'connector.stroke': '#000000'
}
editorSetting.set('connector.stroke', '#000')
```

and use `expandFlattenObject` function can restore the flattened structure to a nested structure. nested structure is required by blocksuite

```ts
editorSetting.settings$.map(expandFlattenObject) === {
  connector: {
    stroke: '#000000'
  }
}
```
2024-08-26 08:19:24 +00:00
JimmFly
3c37006657 chore(core): add Display bi-directional links setting row ui (#7954)
close AF-1269

![CleanShot 2024-08-23 at 12 59 50@2x](https://github.com/user-attachments/assets/bcdc70c4-029e-4254-8367-a390a244e30b)
2024-08-26 06:25:32 +00:00
Don Isaac
dbcfd24ed8 fix: remove unused variables (#7968) 2024-08-26 14:23:56 +08:00
forehalo
14066965fa fix(server): wrong table used for userspace data (#7969) 2024-08-26 06:11:22 +00:00
renovate
01c9d1758e chore: bump up Rust crate sqlx to 0.8 [SECURITY] (#7965)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [sqlx](https://togithub.com/launchbadge/sqlx) | workspace.dependencies | minor | `0.7` -> `0.8` |

### GitHub Vulnerability Alerts

#### [GHSA-xmrp-424f-vfpx](https://togithub.com/launchbadge/sqlx/issues/3440)

The following presentation at this year's DEF CON was brought to our attention on the SQLx Discord:

> SQL Injection isn't Dead: Smuggling Queries at the Protocol Level
> <http://web.archive.org/web/20240812130923/https://media.defcon.org/DEF%20CON%2032/DEF%20CON%2032%20presentations/DEF%20CON%2032%20-%20Paul%20Gerste%20-%20SQL%20Injection%20Isn't%20Dead%20Smuggling%20Queries%20at%20the%20Protocol%20Level.pdf>
> (Archive link for posterity.)

Essentially, encoding a value larger than 4GiB can cause the length prefix in the protocol to overflow,
causing the server to interpret the rest of the string as binary protocol commands or other data.

It appears SQLx _does_ perform truncating casts in a way that could be problematic,
for example: <6f2905695b/sqlx-postgres/src/arguments.rs (L163)>

This code has existed essentially since the beginning,
so it is reasonable to assume that all published versions `<= 0.8.0` are affected.

## Mitigation

As always, you should make sure your application is validating untrustworthy user input.
Reject any input over 4 GiB, or any input that could _encode_ to a string longer than 4 GiB.
Dynamically built queries are also potentially problematic if it pushes the message size over this 4 GiB bound.

[`Encode::size_hint()`](https://docs.rs/sqlx/latest/sqlx/trait.Encode.html#method.size_hint)
can be used for sanity checks, but do not assume that the size returned is accurate.
For example, the `Json<T>` and `Text<T>` adapters have no reasonable way to predict or estimate the final encoded size,
so they just return `size_of::<T>()` instead.

For web application backends, consider adding some middleware that limits the size of request bodies by default.

## Resolution

Work has started on a branch to add `#[deny]` directives for the following Clippy lints:

* [`cast_possible_truncation`](https://rust-lang.github.io/rust-clippy/master/#/cast_possible_truncation)
* [`cast_possible_wrap`](https://rust-lang.github.io/rust-clippy/master/#/cast_possible_wrap)
* [`cast_sign_loss`](https://rust-lang.github.io/rust-clippy/master/#/cast_sign_loss)

and to manually audit the code that they flag.

A fix is expected to be included in the `0.8.1` release (still WIP as of writing).

---

### Release Notes

<details>
<summary>launchbadge/sqlx (sqlx)</summary>

### [`v0.8.1`](https://togithub.com/launchbadge/sqlx/blob/HEAD/CHANGELOG.md#081---2024-08-23)

[Compare Source](https://togithub.com/launchbadge/sqlx/compare/v0.8.0...v0.8.1)

16 pull requests were merged this release cycle.

This release contains a fix for [RUSTSEC-2024-0363].

Postgres users are advised to upgrade ASAP as a possible exploit has been demonstrated:
[#&#8203;3440 (comment)](https://togithub.com/launchbadge/sqlx/issues/3440#issuecomment-2307956901)

MySQL and SQLite do not *appear* to be exploitable, but upgrading is recommended nonetheless.

##### Added

-   \[[#&#8203;3421]]: correct spelling of `MySqlConnectOptions::no_engine_substitution()` \[\[[@&#8203;kolinfluence](https://togithub.com/kolinfluence)]]
    -   Deprecates `MySqlConnectOptions::no_engine_subsitution()` (oops) in favor of the correctly spelled version.

##### Changed

-   \[[#&#8203;3376]]: doc: hide `spec_error` module \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
    -   This is a helper module for the macros and was not meant to be exposed.
    -   It is not expected to receive any breaking changes for the 0.8.x release, but is not designed as a public API.
        Use at your own risk.
-   \[[#&#8203;3382]]: feat: bumped to `libsqlite3-sys=0.30.1` to support sqlite 3.46 \[\[[@&#8203;CommanderStorm](https://togithub.com/CommanderStorm)]]
-   \[[#&#8203;3385]]: chore(examples):Migrated the pg-chat example to ratatui \[\[[@&#8203;CommanderStorm](https://togithub.com/CommanderStorm)]]
-   \[[#&#8203;3399]]: Upgrade to rustls 0.23 \[\[[@&#8203;djc](https://togithub.com/djc)]]
    -   RusTLS now has pluggable cryptography providers: `ring` (the existing implementation),
        and `aws-lc-rs` which has optional FIPS certification.
    -   The existing features activating RusTLS (`runtime-tokio-rustls`, `runtime-async-std-rustls`, `tls-rustls`)
        enable the `ring` provider of RusTLS to match the existing behavior so this *should not* be a breaking change.
    -   Switch to the `tls-rustls-aws-lc-rs` feature to use the `aws-lc-rs` provider.
        -   If using `runtime-tokio-rustls` or `runtime-async-std-rustls`,
            this will necessitate switching to the appropriate non-legacy runtime feature:
            `runtime-tokio` or `runtime-async-std`
    -   See the RusTLS README for more details: <https://github.com/rustls/rustls?tab=readme-ov-file#cryptography-providers>

##### Fixed

-   \[[#&#8203;2786]]: fix(sqlx-cli): do not clean sqlx during prepare \[\[[@&#8203;cycraig](https://togithub.com/cycraig)]]
-   \[[#&#8203;3354]]: sqlite: fix inconsistent read-after-write \[\[[@&#8203;ckampfe](https://togithub.com/ckampfe)]]
-   \[[#&#8203;3371]]: Fix encoding and decoding of MySQL enums in `sqlx::Type` \[\[[@&#8203;alu](https://togithub.com/alu)]]
-   \[[#&#8203;3374]]: fix: usage of `node12` in `SQLx` action \[\[[@&#8203;hamirmahal](https://togithub.com/hamirmahal)]]
-   \[[#&#8203;3380]]: chore: replace structopt with clap in examples \[\[[@&#8203;tottoto](https://togithub.com/tottoto)]]
-   \[[#&#8203;3381]]: Fix CI after Rust 1.80, remove dead feature references \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
-   \[[#&#8203;3384]]: chore(tests): fixed deprecation warnings \[\[[@&#8203;CommanderStorm](https://togithub.com/CommanderStorm)]]
-   \[[#&#8203;3386]]: fix(dependencys):bumped cargo_metadata to `v0.18.1` to avoid yanked `v0.14.3` \[\[[@&#8203;CommanderStorm](https://togithub.com/CommanderStorm)]]
-   \[[#&#8203;3389]]: fix(cli): typo in error for required DB URL \[\[[@&#8203;ods](https://togithub.com/ods)]]
-   \[[#&#8203;3417]]: Update version to 0.8 in README \[\[[@&#8203;soucosmo](https://togithub.com/soucosmo)]]
-   \[[#&#8203;3441]]: fix: audit protocol handling \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
    -   This addresses [RUSTSEC-2024-0363] and includes regression tests for MySQL, Postgres and SQLite.

[#&#8203;2786]: https://togithub.com/launchbadge/sqlx/pull/2786

[#&#8203;3354]: https://togithub.com/launchbadge/sqlx/pull/3354

[#&#8203;3371]: https://togithub.com/launchbadge/sqlx/pull/3371

[#&#8203;3374]: https://togithub.com/launchbadge/sqlx/pull/3374

[#&#8203;3376]: https://togithub.com/launchbadge/sqlx/pull/3376

[#&#8203;3380]: https://togithub.com/launchbadge/sqlx/pull/3380

[#&#8203;3381]: https://togithub.com/launchbadge/sqlx/pull/3381

[#&#8203;3382]: https://togithub.com/launchbadge/sqlx/pull/3382

[#&#8203;3384]: https://togithub.com/launchbadge/sqlx/pull/3384

[#&#8203;3385]: https://togithub.com/launchbadge/sqlx/pull/3385

[#&#8203;3386]: https://togithub.com/launchbadge/sqlx/pull/3386

[#&#8203;3389]: https://togithub.com/launchbadge/sqlx/pull/3389

[#&#8203;3399]: https://togithub.com/launchbadge/sqlx/pull/3399

[#&#8203;3417]: https://togithub.com/launchbadge/sqlx/pull/3417

[#&#8203;3421]: https://togithub.com/launchbadge/sqlx/pull/3421

[#&#8203;3441]: https://togithub.com/launchbadge/sqlx/pull/3441

[RUSTSEC-2024-0363]: https://rustsec.org/advisories/RUSTSEC-2024-0363.html

### [`v0.8.0`](https://togithub.com/launchbadge/sqlx/blob/HEAD/CHANGELOG.md#080---2024-07-22)

[Compare Source](https://togithub.com/launchbadge/sqlx/compare/v0.7.4...v0.8.0)

70 pull requests were merged this release cycle.

[#&#8203;2697] was merged the same day as release 0.7.4 and so was missed by the automatic CHANGELOG generation.

##### Breaking

-   \[[#&#8203;2697]]: fix(macros): only enable chrono when time is disabled \[\[[@&#8203;saiintbrisson](https://togithub.com/saiintbrisson)]]
-   \[[#&#8203;2973]]: Generic Associated Types in Database, replacing HasValueRef, HasArguments, HasStatement \[\[[@&#8203;nitn3lav](https://togithub.com/nitn3lav)]]
-   \[[#&#8203;2482]]: chore: bump syn to 2.0 \[\[[@&#8203;saiintbrisson](https://togithub.com/saiintbrisson)]]
    -   Deprecated type ascription syntax in the query macros was removed.
-   \[[#&#8203;2736]]: Fix describe on PostgreSQL views with rules \[\[[@&#8203;tsing](https://togithub.com/tsing)]]
    -   Potentially breaking: nullability inference changes for Postgres.
-   \[[#&#8203;2869]]: Implement PgHasArrayType for all references \[\[[@&#8203;tylerhawkes](https://togithub.com/tylerhawkes)]]
    -   Conflicts with existing manual implementations.
-   \[[#&#8203;2940]]: fix: Decode and Encode derives ([#&#8203;1031](https://togithub.com/launchbadge/sqlx/issues/1031)) \[\[[@&#8203;benluelo](https://togithub.com/benluelo)]]
    -   Changes lifetime obligations for field types.
-   \[[#&#8203;3064]]: Sqlite explain graph \[\[[@&#8203;tyrelr](https://togithub.com/tyrelr)]]
    -   Potentially breaking: nullability inference changes for SQLite.
-   \[[#&#8203;3123]]: Reorder attrs in sqlx::test macro \[\[[@&#8203;bobozaur](https://togithub.com/bobozaur)]]
    -   Potentially breaking: attributes on `#[sqlx::test]` usages are applied in the correct order now.
-   \[[#&#8203;3126]]: Make Encode return a result \[\[[@&#8203;FSMaxB](https://togithub.com/FSMaxB)]]
-   \[[#&#8203;3130]]: Add version information for failed cli migration ([#&#8203;3129](https://togithub.com/launchbadge/sqlx/issues/3129)) \[\[[@&#8203;FlakM](https://togithub.com/FlakM)]]
    -   Breaking changes to `MigrateError`.
-   \[[#&#8203;3181]]: feat: no tx migration \[\[[@&#8203;cleverjam](https://togithub.com/cleverjam)]]
    -   (Postgres only) migrations that should not run in a transaction can be flagged by adding `-- no-transaction` to the beginning.
    -   Breaking change: added field to `Migration`
-   \[[#&#8203;3184]]: \[BREAKING} fix(sqlite): always use `i64` as intermediate when decoding \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
    -   integer decoding will now loudly error on overflow instead of silently truncating.
    -   some usages of the query!() macros might change an i32 to an i64.
-   \[[#&#8203;3252]]: fix `#[derive(sqlx::Type)]` in Postgres \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
    -   Manual implementations of PgHasArrayType for enums will conflict with the generated one. Delete the manual impl or add `#[sqlx(no_pg_array)]` where conflicts occur.
    -   Type equality for PgTypeInfo is now schema-aware.
-   \[[#&#8203;3329]]: fix: correct handling of arrays of custom types in Postgres \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
    -   Potential breaking change: `PgTypeInfo::with_name()` infers types that start with `_` to be arrays of the un-prefixed type. Wrap type names in quotes to bypass this behavior.
-   \[[#&#8203;3356]]: breaking: fix name collision in `FromRow`, return `Error::ColumnDecode` for `TryFrom` errors \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
    -   Breaking behavior change: errors with `#[sqlx(try_from = "T")]` now return `Error::ColumnDecode` instead of `Error::ColumnNotFound`.
    -   Breaking because `#[sqlx(default)]` on an individual field or the struct itself would have previously suppressed the error.
        This doesn't seem like good behavior as it could result in some potentially very difficult bugs.
        -   Instead, create a wrapper implementing `From` and apply the default explicitly.
-   \[[#&#8203;3337]]: allow rename with rename_all (close [#&#8203;2896](https://togithub.com/launchbadge/sqlx/issues/2896)) \[\[[@&#8203;DirectorX](https://togithub.com/DirectorX)]]
    -   Changes the precedence of `#[sqlx(rename)]` and `#[sqlx(rename_all)]` to match the expected behavior (`rename` wins).
-   \[[#&#8203;3285]]: fix: use correct names for sslmode options \[\[[@&#8203;lily-mosquitoes](https://togithub.com/lily-mosquitoes)]]
    -   Changes the output of `ConnectOptions::to_url_lossy()` to match what parsing expects.

##### Added

-   \[[#&#8203;2917]]: Add Debug impl for PgRow \[\[[@&#8203;g-bartoszek](https://togithub.com/g-bartoszek)]]
-   \[[#&#8203;3113]]: feat: new derive feature flag \[\[[@&#8203;saiintbrisson](https://togithub.com/saiintbrisson)]]
-   \[[#&#8203;3154]]: feat: add `MySqlTime`, audit `mysql::types` for panics \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
-   \[[#&#8203;3188]]: feat(cube): support postgres cube \[\[[@&#8203;jayy-lmao](https://togithub.com/jayy-lmao)]]
-   \[[#&#8203;3244]]: feat: support `NonZero*` scalar types \[\[[@&#8203;AlphaKeks](https://togithub.com/AlphaKeks)]]
-   \[[#&#8203;3260]]: feat: Add set_update_hook on SqliteConnection \[\[[@&#8203;gridbox](https://togithub.com/gridbox)]]
-   \[[#&#8203;3291]]: feat: support the Postgres Bool type for the Any driver \[\[[@&#8203;etorreborre](https://togithub.com/etorreborre)]]
-   \[[#&#8203;3293]]: Add LICENSE-\* files to crates \[\[[@&#8203;LecrisUT](https://togithub.com/LecrisUT)]]
-   \[[#&#8203;3303]]: add array support for NonZeroI\* in postgres \[\[[@&#8203;JohannesIBK](https://togithub.com/JohannesIBK)]]
-   \[[#&#8203;3311]]: Add example on how to use Transaction as Executor \[\[[@&#8203;Lachstec](https://togithub.com/Lachstec)]]
-   \[[#&#8203;3343]]: Add support for PostgreSQL HSTORE data type \[\[[@&#8203;KobusEllis](https://togithub.com/KobusEllis)]]

##### Changed

-   \[[#&#8203;2652]]: MySQL: Remove collation compatibility check for strings \[\[[@&#8203;alu](https://togithub.com/alu)]]
-   \[[#&#8203;2960]]: Removed `Send` trait bound from argument binding \[\[[@&#8203;bobozaur](https://togithub.com/bobozaur)]]
-   \[[#&#8203;2970]]: refactor: lift type mappings into driver crates \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
-   \[[#&#8203;3148]]: Bump libsqlite3-sys to v0.28 \[\[[@&#8203;NfNitLoop](https://togithub.com/NfNitLoop)]]
    -   Note: version bumps to `libsqlite3-sys` are not considered breaking changes as per our semver guarantees.
-   \[[#&#8203;3265]]: perf: box `MySqlConnection` to reduce sizes of futures \[\[[@&#8203;stepantubanov](https://togithub.com/stepantubanov)]]
-   \[[#&#8203;3352]]: chore:added a testcase for `sqlx migrate add ...` \[\[[@&#8203;CommanderStorm](https://togithub.com/CommanderStorm)]]
-   \[[#&#8203;3340]]: ci: Add job to check that sqlx builds with its declared minimum dependencies \[\[[@&#8203;iamjpotts](https://togithub.com/iamjpotts)]]

##### Fixed

-   \[[#&#8203;2702]]: Constrain cyclic associated types to themselves \[\[[@&#8203;BadBastion](https://togithub.com/BadBastion)]]
-   \[[#&#8203;2954]]: Fix several inter doc links \[\[[@&#8203;ralpha](https://togithub.com/ralpha)]]
-   \[[#&#8203;3073]]: feat(logging): Log slow acquires from connection pool \[\[[@&#8203;iamjpotts](https://togithub.com/iamjpotts)]]
-   \[[#&#8203;3137]]: SqliteConnectOptions::filename() memory fix ([#&#8203;3136](https://togithub.com/launchbadge/sqlx/issues/3136)) \[\[[@&#8203;hoxxep](https://togithub.com/hoxxep)]]
-   \[[#&#8203;3138]]: PostgreSQL Bugfix: Ensure connection is usable after failed COPY inside a transaction \[\[[@&#8203;feikesteenbergen](https://togithub.com/feikesteenbergen)]]
-   \[[#&#8203;3146]]: fix(sqlite): delete unused `ConnectionHandleRaw` type \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
-   \[[#&#8203;3162]]: Drop urlencoding dependency \[\[[@&#8203;paolobarbolini](https://togithub.com/paolobarbolini)]]
-   \[[#&#8203;3165]]: Bump deps that do not need code changes \[\[[@&#8203;GnomedDev](https://togithub.com/GnomedDev)]]
-   \[[#&#8203;3167]]: fix(ci): use `docker compose` instead of `docker-compose` \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
-   \[[#&#8203;3172]]: fix: Option decoding in any driver \[\[[@&#8203;pxp9](https://togithub.com/pxp9)]]
-   \[[#&#8203;3173]]: fix(postgres) : int type conversion while decoding \[\[[@&#8203;RaghavRox](https://togithub.com/RaghavRox)]]
-   \[[#&#8203;3190]]: Update time to 0.3.36 \[\[[@&#8203;BlackSoulHub](https://togithub.com/BlackSoulHub)]]
-   \[[#&#8203;3191]]: Fix unclean TLS shutdown \[\[[@&#8203;levkk](https://togithub.com/levkk)]]
-   \[[#&#8203;3194]]: Fix leaking connections in fetch_optional ([#&#8203;2647](https://togithub.com/launchbadge/sqlx/issues/2647)) \[\[[@&#8203;danjpgriffin](https://togithub.com/danjpgriffin)]]
-   \[[#&#8203;3216]]: security: bump rustls to 0.21.11 \[\[[@&#8203;toxeus](https://togithub.com/toxeus)]]
-   \[[#&#8203;3230]]: fix: sqlite pragma order for auto_vacuum \[\[[@&#8203;jasonish](https://togithub.com/jasonish)]]
-   \[[#&#8203;3233]]: fix: get_filename should not consume self \[\[[@&#8203;jasonish](https://togithub.com/jasonish)]]
-   \[[#&#8203;3234]]: fix(ci): pin Rust version, ditch unmaintained actions \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
-   \[[#&#8203;3236]]: fix: resolve `path` ownership problems when using `sqlx_macros_unstable` \[\[[@&#8203;lily-mosquitoes](https://togithub.com/lily-mosquitoes)]]
-   \[[#&#8203;3254]]: fix: hide `sqlx_postgres::any` \[\[[@&#8203;Zarathustra2](https://togithub.com/Zarathustra2)]]
-   \[[#&#8203;3266]]: ci: MariaDB - add back 11.4 and add 11.5 \[\[[@&#8203;grooverdan](https://togithub.com/grooverdan)]]
-   \[[#&#8203;3267]]: ci: syntax fix \[\[[@&#8203;grooverdan](https://togithub.com/grooverdan)]]
-   \[[#&#8203;3271]]: docs(sqlite): fix typo - unixtime() -> unixepoch() \[\[[@&#8203;joelkoen](https://togithub.com/joelkoen)]]
-   \[[#&#8203;3276]]: Invert boolean for `migrate` error message. ([#&#8203;3275](https://togithub.com/launchbadge/sqlx/issues/3275)) \[\[[@&#8203;nk9](https://togithub.com/nk9)]]
-   \[[#&#8203;3279]]: fix Clippy errors \[\[[@&#8203;abonander](https://togithub.com/abonander)]]
-   \[[#&#8203;3288]]: fix: sqlite update_hook char types \[\[[@&#8203;jasonish](https://togithub.com/jasonish)]]
-   \[[#&#8203;3297]]: Pass the `persistent` query setting when preparing queries with the `Any` driver \[\[[@&#8203;etorreborre](https://togithub.com/etorreborre)]]
-   \[[#&#8203;3298]]: Track null arguments in order to provide the appropriate type when converting them. \[\[[@&#8203;etorreborre](https://togithub.com/etorreborre)]]
-   \[[#&#8203;3312]]: doc: Minor rust docs fixes \[\[[@&#8203;SrGesus](https://togithub.com/SrGesus)]]
-   \[[#&#8203;3327]]: chore: fixed one usage of `select_input_type!()` being unhygenic \[\[[@&#8203;CommanderStorm](https://togithub.com/CommanderStorm)]]
-   \[[#&#8203;3328]]: fix(ci): comment not separated from other characters \[\[[@&#8203;hamirmahal](https://togithub.com/hamirmahal)]]
-   \[[#&#8203;3341]]: refactor: Resolve cargo check warnings in postgres examples \[\[[@&#8203;iamjpotts](https://togithub.com/iamjpotts)]]
-   \[[#&#8203;3346]]: fix(postgres): don't panic if `M` or `C` Notice fields are not UTF-8 \[\[[@&#8203;YgorSouza](https://togithub.com/YgorSouza)]]
-   \[[#&#8203;3350]]: fix:the `json`-feature should activate `sqlx-postgres?/json` as well \[\[[@&#8203;CommanderStorm](https://togithub.com/CommanderStorm)]]
-   \[[#&#8203;3353]]: fix: build script new line at eof \[\[[@&#8203;Zarthus](https://togithub.com/Zarthus)]]
-   (no PR): activate `clock` and `std` features of `workspace.dependencies.chrono`.

[#&#8203;2482]: https://togithub.com/launchbadge/sqlx/pull/2482

[#&#8203;2652]: https://togithub.com/launchbadge/sqlx/pull/2652

[#&#8203;2697]: https://togithub.com/launchbadge/sqlx/pull/2697

[#&#8203;2702]: https://togithub.com/launchbadge/sqlx/pull/2702

[#&#8203;2736]: https://togithub.com/launchbadge/sqlx/pull/2736

[#&#8203;2869]: https://togithub.com/launchbadge/sqlx/pull/2869

[#&#8203;2917]: https://togithub.com/launchbadge/sqlx/pull/2917

[#&#8203;2940]: https://togithub.com/launchbadge/sqlx/pull/2940

[#&#8203;2954]: https://togithub.com/launchbadge/sqlx/pull/2954

[#&#8203;2960]: https://togithub.com/launchbadge/sqlx/pull/2960

[#&#8203;2970]: https://togithub.com/launchbadge/sqlx/pull/2970

[#&#8203;2973]: https://togithub.com/launchbadge/sqlx/pull/2973

[#&#8203;3064]: https://togithub.com/launchbadge/sqlx/pull/3064

[#&#8203;3073]: https://togithub.com/launchbadge/sqlx/pull/3073

[#&#8203;3113]: https://togithub.com/launchbadge/sqlx/pull/3113

[#&#8203;3123]: https://togithub.com/launchbadge/sqlx/pull/3123

[#&#8203;3126]: https://togithub.com/launchbadge/sqlx/pull/3126

[#&#8203;3130]: https://togithub.com/launchbadge/sqlx/pull/3130

[#&#8203;3137]: https://togithub.com/launchbadge/sqlx/pull/3137

[#&#8203;3138]: https://togithub.com/launchbadge/sqlx/pull/3138

[#&#8203;3146]: https://togithub.com/launchbadge/sqlx/pull/3146

[#&#8203;3148]: https://togithub.com/launchbadge/sqlx/pull/3148

[#&#8203;3154]: https://togithub.com/launchbadge/sqlx/pull/3154

[#&#8203;3162]: https://togithub.com/launchbadge/sqlx/pull/3162

[#&#8203;3165]: https://togithub.com/launchbadge/sqlx/pull/3165

[#&#8203;3167]: https://togithub.com/launchbadge/sqlx/pull/3167

[#&#8203;3172]: https://togithub.com/launchbadge/sqlx/pull/3172

[#&#8203;3173]: https://togithub.com/launchbadge/sqlx/pull/3173

[#&#8203;3181]: https://togithub.com/launchbadge/sqlx/pull/3181

[#&#8203;3184]: https://togithub.com/launchbadge/sqlx/pull/3184

[#&#8203;3188]: https://togithub.com/launchbadge/sqlx/pull/3188

[#&#8203;3190]: https://togithub.com/launchbadge/sqlx/pull/3190

[#&#8203;3191]: https://togithub.com/launchbadge/sqlx/pull/3191

[#&#8203;3194]: https://togithub.com/launchbadge/sqlx/pull/3194

[#&#8203;3216]: https://togithub.com/launchbadge/sqlx/pull/3216

[#&#8203;3230]: https://togithub.com/launchbadge/sqlx/pull/3230

[#&#8203;3233]: https://togithub.com/launchbadge/sqlx/pull/3233

[#&#8203;3234]: https://togithub.com/launchbadge/sqlx/pull/3234

[#&#8203;3236]: https://togithub.com/launchbadge/sqlx/pull/3236

[#&#8203;3244]: https://togithub.com/launchbadge/sqlx/pull/3244

[#&#8203;3252]: https://togithub.com/launchbadge/sqlx/pull/3252

[#&#8203;3254]: https://togithub.com/launchbadge/sqlx/pull/3254

[#&#8203;3260]: https://togithub.com/launchbadge/sqlx/pull/3260

[#&#8203;3265]: https://togithub.com/launchbadge/sqlx/pull/3265

[#&#8203;3266]: https://togithub.com/launchbadge/sqlx/pull/3266

[#&#8203;3267]: https://togithub.com/launchbadge/sqlx/pull/3267

[#&#8203;3271]: https://togithub.com/launchbadge/sqlx/pull/3271

[#&#8203;3276]: https://togithub.com/launchbadge/sqlx/pull/3276

[#&#8203;3279]: https://togithub.com/launchbadge/sqlx/pull/3279

[#&#8203;3285]: https://togithub.com/launchbadge/sqlx/pull/3285

[#&#8203;3288]: https://togithub.com/launchbadge/sqlx/pull/3288

[#&#8203;3291]: https://togithub.com/launchbadge/sqlx/pull/3291

[#&#8203;3293]: https://togithub.com/launchbadge/sqlx/pull/3293

[#&#8203;3297]: https://togithub.com/launchbadge/sqlx/pull/3297

[#&#8203;3298]: https://togithub.com/launchbadge/sqlx/pull/3298

[#&#8203;3303]: https://togithub.com/launchbadge/sqlx/pull/3303

[#&#8203;3311]: https://togithub.com/launchbadge/sqlx/pull/3311

[#&#8203;3312]: https://togithub.com/launchbadge/sqlx/pull/3312

[#&#8203;3327]: https://togithub.com/launchbadge/sqlx/pull/3327

[#&#8203;3328]: https://togithub.com/launchbadge/sqlx/pull/3328

[#&#8203;3329]: https://togithub.com/launchbadge/sqlx/pull/3329

[#&#8203;3337]: https://togithub.com/launchbadge/sqlx/pull/3337

[#&#8203;3340]: https://togithub.com/launchbadge/sqlx/pull/3340

[#&#8203;3341]: https://togithub.com/launchbadge/sqlx/pull/3341

[#&#8203;3343]: https://togithub.com/launchbadge/sqlx/pull/3343

[#&#8203;3346]: https://togithub.com/launchbadge/sqlx/pull/3346

[#&#8203;3350]: https://togithub.com/launchbadge/sqlx/pull/3350

[#&#8203;3352]: https://togithub.com/launchbadge/sqlx/pull/3352

[#&#8203;3353]: https://togithub.com/launchbadge/sqlx/pull/3353

[#&#8203;3356]: https://togithub.com/launchbadge/sqlx/pull/3356

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-24 12:47:32 +00:00
renovate
130dc2bd8b chore: bump up oxlint version to v0.8.0 (#7962)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [oxlint](https://oxc.rs) ([source](https://togithub.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`0.7.2` -> `0.8.0`](https://renovatebot.com/diffs/npm/oxlint/0.7.2/0.8.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/0.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxlint/0.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxlint/0.7.2/0.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/0.7.2/0.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v0.8.0`](https://togithub.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#080---2024-08-23)

[Compare Source](b3e189764f...8ef85a43c0)

##### Features

-   [`a0effab`](https://togithub.com/oxc-project/oxc/commit/a0effab) linter: Support more flexible config.globals values ([#&#8203;4990](https://togithub.com/oxc-project/oxc/issues/4990)) (Don Isaac)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-23 16:19:40 +00:00
renovate
442a843257 chore: bump up all non-major dependencies (#7953)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence | Type | Update |
|---|---|---|---|---|---|---|---|
| [@aws-sdk/client-s3](https://togithub.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) ([source](https://togithub.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)) | [`3.635.0` -> `3.637.0`](https://renovatebot.com/diffs/npm/@aws-sdk%2fclient-s3/3.635.0/3.637.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@aws-sdk%2fclient-s3/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@aws-sdk%2fclient-s3/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@aws-sdk%2fclient-s3/3.635.0/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@aws-sdk%2fclient-s3/3.635.0/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@aws-sdk/client-s3](https://togithub.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) ([source](https://togithub.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)) | [`3.635.0` -> `3.637.0`](https://renovatebot.com/diffs/npm/@aws-sdk%2fclient-s3/3.635.0/3.637.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@aws-sdk%2fclient-s3/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@aws-sdk%2fclient-s3/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@aws-sdk%2fclient-s3/3.635.0/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@aws-sdk%2fclient-s3/3.635.0/3.637.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@keyv/redis](https://togithub.com/jaredwray/keyv) | [`3.0.0` -> `3.0.1`](https://renovatebot.com/diffs/npm/@keyv%2fredis/3.0.0/3.0.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@keyv%2fredis/3.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@keyv%2fredis/3.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@keyv%2fredis/3.0.0/3.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@keyv%2fredis/3.0.0/3.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [@napi-rs/simple-git](https://togithub.com/Brooooooklyn/simple-git) | [`0.1.17` -> `0.1.18`](https://renovatebot.com/diffs/npm/@napi-rs%2fsimple-git/0.1.17/0.1.18) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@napi-rs%2fsimple-git/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@napi-rs%2fsimple-git/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@napi-rs%2fsimple-git/0.1.17/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@napi-rs%2fsimple-git/0.1.17/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@napi-rs/simple-git](https://togithub.com/Brooooooklyn/simple-git) | [`0.1.17` -> `0.1.18`](https://renovatebot.com/diffs/npm/@napi-rs%2fsimple-git/0.1.17/0.1.18) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@napi-rs%2fsimple-git/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@napi-rs%2fsimple-git/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@napi-rs%2fsimple-git/0.1.17/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@napi-rs%2fsimple-git/0.1.17/0.1.18?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [@nx/vite](https://nx.dev) ([source](https://togithub.com/nrwl/nx/tree/HEAD/packages/vite)) | [`19.6.1` -> `19.6.2`](https://renovatebot.com/diffs/npm/@nx%2fvite/19.6.1/19.6.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nx%2fvite/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nx%2fvite/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nx%2fvite/19.6.1/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nx%2fvite/19.6.1/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@opentelemetry/semantic-conventions](https://togithub.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions) ([source](https://togithub.com/open-telemetry/opentelemetry-js)) | [`1.25.1` -> `1.26.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsemantic-conventions/1.25.1/1.26.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fsemantic-conventions/1.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fsemantic-conventions/1.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fsemantic-conventions/1.25.1/1.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fsemantic-conventions/1.25.1/1.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [foxact](https://foxact.skk.moe) ([source](https://togithub.com/SukkaW/foxact)) | [`0.2.36` -> `0.2.37`](https://renovatebot.com/diffs/npm/foxact/0.2.36/0.2.37) | [![age](https://developer.mend.io/api/mc/badges/age/npm/foxact/0.2.37?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/foxact/0.2.37?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/foxact/0.2.36/0.2.37?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/foxact/0.2.36/0.2.37?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [lucide-react](https://lucide.dev) ([source](https://togithub.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react)) | [`^0.429.0` -> `^0.435.0`](https://renovatebot.com/diffs/npm/lucide-react/0.429.0/0.435.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-react/0.435.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lucide-react/0.435.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lucide-react/0.429.0/0.435.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-react/0.429.0/0.435.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [node](https://nodejs.org) ([source](https://togithub.com/nodejs/node)) | `20.16.0` -> `20.17.0` | [![age](https://developer.mend.io/api/mc/badges/age/node-version/node/v20.17.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/node-version/node/v20.17.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/node-version/node/v20.16.0/v20.17.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/node-version/node/v20.16.0/v20.17.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |  | minor |
| [nx](https://nx.dev) ([source](https://togithub.com/nrwl/nx/tree/HEAD/packages/nx)) | [`19.6.1` -> `19.6.2`](https://renovatebot.com/diffs/npm/nx/19.6.1/19.6.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nx/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nx/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nx/19.6.1/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nx/19.6.1/19.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [react-resizable-panels](https://togithub.com/bvaughn/react-resizable-panels) | [`2.1.0` -> `2.1.1`](https://renovatebot.com/diffs/npm/react-resizable-panels/2.1.0/2.1.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-resizable-panels/2.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-resizable-panels/2.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-resizable-panels/2.1.0/2.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-resizable-panels/2.1.0/2.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [react-transition-state](https://szhsin.github.io/react-transition-state/) ([source](https://togithub.com/szhsin/react-transition-state)) | [`2.1.1` -> `2.1.2`](https://renovatebot.com/diffs/npm/react-transition-state/2.1.1/2.1.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-transition-state/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-transition-state/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-transition-state/2.1.1/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-transition-state/2.1.1/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [webpack](https://togithub.com/webpack/webpack) | [`5.93.0` -> `5.94.0`](https://renovatebot.com/diffs/npm/webpack/5.93.0/5.94.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/webpack/5.94.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/webpack/5.94.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/webpack/5.93.0/5.94.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/webpack/5.93.0/5.94.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [wrangler](https://togithub.com/cloudflare/workers-sdk) ([source](https://togithub.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler)) | [`3.72.1` -> `3.72.2`](https://renovatebot.com/diffs/npm/wrangler/3.72.1/3.72.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/3.72.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/wrangler/3.72.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/wrangler/3.72.1/3.72.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/3.72.1/3.72.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |

---

### Release Notes

<details>
<summary>aws/aws-sdk-js-v3 (@&#8203;aws-sdk/client-s3)</summary>

### [`v3.637.0`](https://togithub.com/aws/aws-sdk-js-v3/blob/HEAD/clients/client-s3/CHANGELOG.md#36370-2024-08-22)

[Compare Source](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.635.0...v3.637.0)

**Note:** Version bump only for package [@&#8203;aws-sdk/client-s3](https://togithub.com/aws-sdk/client-s3)

</details>

<details>
<summary>Brooooooklyn/simple-git (@&#8203;napi-rs/simple-git)</summary>

### [`v0.1.18`](https://togithub.com/Brooooooklyn/simple-git/releases/tag/v0.1.18)

[Compare Source](https://togithub.com/Brooooooklyn/simple-git/compare/v0.1.17...v0.1.18)

##### Core features:

##### Blob API

```js
import { Repository } from "@&#8203;napi-rs/simple-git";

const repo = new Repository(".");

const blob = repo.head().peelToTree()
    .getPath("__test__/repo.spec.mjs")
    .toObject(repo)
    .peelToBlob();

const fileContent = Buffer.from(blob.content()).toString("utf8");
```

##### What's Changed

-   chore(deps): update yarn to v4.4.0 by [@&#8203;renovate](https://togithub.com/renovate) in [https://github.com/Brooooooklyn/simple-git/pull/58](https://togithub.com/Brooooooklyn/simple-git/pull/58)
-   chore(deps): lock file maintenance by [@&#8203;renovate](https://togithub.com/renovate) in [https://github.com/Brooooooklyn/simple-git/pull/57](https://togithub.com/Brooooooklyn/simple-git/pull/57)
-   feat: implment blob and related features by [@&#8203;Brooooooklyn](https://togithub.com/Brooooooklyn) in [https://github.com/Brooooooklyn/simple-git/pull/59](https://togithub.com/Brooooooklyn/simple-git/pull/59)

**Full Changelog**: https://github.com/Brooooooklyn/simple-git/compare/v0.1.17...v0.1.18

</details>

<details>
<summary>nrwl/nx (@&#8203;nx/vite)</summary>

### [`v19.6.2`](https://togithub.com/nrwl/nx/releases/tag/19.6.2)

[Compare Source](https://togithub.com/nrwl/nx/compare/19.6.1...19.6.2)

##### 19.6.2 (2024-08-21)

##### 🚀 Features

-   **bundling:** add option to generate sourcemaps for Rollup build ([#&#8203;27539](https://togithub.com/nrwl/nx/pull/27539))
-   **devkit:** prefer strings over Linter enum ([#&#8203;27209](https://togithub.com/nrwl/nx/pull/27209))
-   **graph:** add expandedTargets to project details on nx dev ([#&#8203;26911](https://togithub.com/nrwl/nx/pull/26911))
-   **js:** add skipPackageManager option to build executors in order to skip generating "packageManager" entry in package.json ([#&#8203;27518](https://togithub.com/nrwl/nx/pull/27518))
-   **nx-dev:** honor prefers-reduced-motion ([#&#8203;27541](https://togithub.com/nrwl/nx/pull/27541))

##### 🩹 Fixes

-   missing export for setRemoteDefinition ([#&#8203;27491](https://togithub.com/nrwl/nx/pull/27491))
-   **core:** fix importing files with special characters ([#&#8203;27484](https://togithub.com/nrwl/nx/pull/27484))
-   **core:** use withVerbose util ([#&#8203;27553](https://togithub.com/nrwl/nx/pull/27553))
-   **core:** support import detection of packages installed from git remote URL ([#&#8203;27569](https://togithub.com/nrwl/nx/pull/27569))
-   **esbuild:** declaration:true should find the correct package root regardless of cwd [#&#8203;26261](https://togithub.com/nrwl/nx/issues/26261) ([#&#8203;27560](https://togithub.com/nrwl/nx/pull/27560), [#&#8203;26261](https://togithub.com/nrwl/nx/issues/26261))
-   **gradle:** track childProjects in properties report ([#&#8203;27488](https://togithub.com/nrwl/nx/pull/27488))
-   **gradle:** fix tasksFileLines might be undefined ([#&#8203;27548](https://togithub.com/nrwl/nx/pull/27548))
-   **js:** only sync references when composite is true, preserve comments in other parts of file ([#&#8203;27530](https://togithub.com/nrwl/nx/pull/27530))
-   **js:** ensure assets option in tsc executor defaults to empty array for programmatic usage ([#&#8203;27565](https://togithub.com/nrwl/nx/pull/27565))
-   **module-federation:** ensure shared packages can be shared from host [#&#8203;27162](https://togithub.com/nrwl/nx/issues/27162) ([#&#8203;27513](https://togithub.com/nrwl/nx/pull/27513), [#&#8203;27162](https://togithub.com/nrwl/nx/issues/27162))
-   **nextjs:** should not fail when running outside of nx cli ([#&#8203;27523](https://togithub.com/nrwl/nx/pull/27523))
-   **nextjs:** Should be able to run custom server targets with swc ([#&#8203;27526](https://togithub.com/nrwl/nx/pull/27526))
-   **nextjs:** schema type for unitTestRunner for library ([#&#8203;26824](https://togithub.com/nrwl/nx/pull/26824))
-   **node:** generate webpack server apps with generatePackageJson:true by default ([#&#8203;27570](https://togithub.com/nrwl/nx/pull/27570))
-   **nx-dev:** modify prompt ([#&#8203;27536](https://togithub.com/nrwl/nx/pull/27536))
-   **nx-plugin:** allow create-package without e2eProject ([#&#8203;27572](https://togithub.com/nrwl/nx/pull/27572))
-   **react:** handle more scenarios when collecting component props for generating stories ([#&#8203;27528](https://togithub.com/nrwl/nx/pull/27528))
-   **storybook:** should generate correct config for nextjs apps [#&#8203;27233](https://togithub.com/nrwl/nx/issues/27233) ([#&#8203;27510](https://togithub.com/nrwl/nx/pull/27510), [#&#8203;27233](https://togithub.com/nrwl/nx/issues/27233))
-   **testing:** fix issues in static server target migrations ([#&#8203;27547](https://togithub.com/nrwl/nx/pull/27547))
-   **vite:** plugin should infer serve target if server config defined [#&#8203;27370](https://togithub.com/nrwl/nx/issues/27370) ([#&#8203;27507](https://togithub.com/nrwl/nx/pull/27507), [#&#8203;27370](https://togithub.com/nrwl/nx/issues/27370))
-   **vite:** load the correct config file from [@&#8203;nx/vite](https://togithub.com/nx/vite):test executor ([#&#8203;27514](https://togithub.com/nrwl/nx/pull/27514))
-   **vite:** add typecheck inferred target for vite plugin [#&#8203;27501](https://togithub.com/nrwl/nx/issues/27501) ([#&#8203;27531](https://togithub.com/nrwl/nx/pull/27531), [#&#8203;27501](https://togithub.com/nrwl/nx/issues/27501))

##### ❤️  Thank You

-   Colum Ferry [@&#8203;Coly010](https://togithub.com/Coly010)
-   Emily Xiong [@&#8203;xiongemi](https://togithub.com/xiongemi)
-   Feliche-Demian Netliukh
-   Guilherme Prezzi [@&#8203;menosprezzi](https://togithub.com/menosprezzi)
-   Isaac Mann [@&#8203;isaacplmann](https://togithub.com/isaacplmann)
-   Jack Hsu [@&#8203;jaysoo](https://togithub.com/jaysoo)
-   James Henry [@&#8203;JamesHenry](https://togithub.com/JamesHenry)
-   Jason Jean [@&#8203;FrozenPandaz](https://togithub.com/FrozenPandaz)
-   Leosvel Pérez Espinosa [@&#8203;leosvelperez](https://togithub.com/leosvelperez)
-   Nicholas Cunningham [@&#8203;ndcunningham](https://togithub.com/ndcunningham)

</details>

<details>
<summary>open-telemetry/opentelemetry-js (@&#8203;opentelemetry/semantic-conventions)</summary>

### [`v1.26.0`](https://togithub.com/open-telemetry/opentelemetry-js/compare/v1.25.1...3cf1c5215f2656ccb82e6a73cd9e6f2782f8d1cc)

[Compare Source](https://togithub.com/open-telemetry/opentelemetry-js/compare/v1.25.1...3cf1c5215f2656ccb82e6a73cd9e6f2782f8d1cc)

</details>

<details>
<summary>SukkaW/foxact (foxact)</summary>

### [`v0.2.37`](https://togithub.com/SukkaW/foxact/blob/HEAD/CHANGELOG.md#0237)

[Compare Source](https://togithub.com/SukkaW/foxact/compare/0.2.36...0.2.37)

**Core Changes**

-   `foxact/rem` now also exports converter factory function
-   `foxact/rem` now supports customize html font size

</details>

<details>
<summary>lucide-icons/lucide (lucide-react)</summary>

### [`v0.435.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.435.0): New icons 0.435.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.434.0...0.435.0)

#### Modified Icons 🔨

-   `milestone` ([#&#8203;2281](https://togithub.com/lucide-icons/lucide/issues/2281)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `signpost` ([#&#8203;2281](https://togithub.com/lucide-icons/lucide/issues/2281)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.434.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.434.0): New icons 0.434.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.433.0...0.434.0)

#### New icons 🎨

-   `volume-off` ([#&#8203;2378](https://togithub.com/lucide-icons/lucide/issues/2378)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `file-volume` ([#&#8203;2378](https://togithub.com/lucide-icons/lucide/issues/2378)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `volume-1` ([#&#8203;2378](https://togithub.com/lucide-icons/lucide/issues/2378)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `volume-2` ([#&#8203;2378](https://togithub.com/lucide-icons/lucide/issues/2378)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `volume-x` ([#&#8203;2378](https://togithub.com/lucide-icons/lucide/issues/2378)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `volume` ([#&#8203;2378](https://togithub.com/lucide-icons/lucide/issues/2378)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.433.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.433.0): New icons 0.433.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.432.0...0.433.0)

#### New icons 🎨

-   `trending-up-down` ([#&#8203;2372](https://togithub.com/lucide-icons/lucide/issues/2372)) by [@&#8203;Alportan](https://togithub.com/Alportan)

#### Fixes Lucide Solid

-   Fixed compilation issues when starting up Vite Dev server by [@&#8203;ericfennis](https://togithub.com/ericfennis) in [https://github.com/lucide-icons/lucide/pull/2375](https://togithub.com/lucide-icons/lucide/pull/2375)

### [`v0.432.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.432.0): New icons 0.432.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.429.0...0.432.0)

#### New icons 🎨

-   `chart-gantt` ([#&#8203;2392](https://togithub.com/lucide-icons/lucide/issues/2392)) by [@&#8203;jguddas](https://togithub.com/jguddas)

#### Modified Icons 🔨

-   `contact-round` ([#&#8203;2391](https://togithub.com/lucide-icons/lucide/issues/2391)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `contact` ([#&#8203;2391](https://togithub.com/lucide-icons/lucide/issues/2391)) by [@&#8203;jguddas](https://togithub.com/jguddas)

</details>

<details>
<summary>nodejs/node (node)</summary>

### [`v20.17.0`](https://togithub.com/nodejs/node/compare/v20.16.0...v20.17.0)

[Compare Source](https://togithub.com/nodejs/node/compare/v20.16.0...v20.17.0)

</details>

<details>
<summary>bvaughn/react-resizable-panels (react-resizable-panels)</summary>

### [`v2.1.1`](https://togithub.com/bvaughn/react-resizable-panels/compare/2.1.0...2.1.1)

[Compare Source](https://togithub.com/bvaughn/react-resizable-panels/compare/2.1.0...2.1.1)

</details>

<details>
<summary>szhsin/react-transition-state (react-transition-state)</summary>

### [`v2.1.2`](https://togithub.com/szhsin/react-transition-state/releases/tag/v2.1.2)

[Compare Source](https://togithub.com/szhsin/react-transition-state/compare/v2.1.1...v2.1.2)

-   Add switch transition examples in README [#&#8203;639](https://togithub.com/szhsin/react-transition-state/issues/639)

</details>

<details>
<summary>webpack/webpack (webpack)</summary>

### [`v5.94.0`](https://togithub.com/webpack/webpack/compare/v5.93.0...eabf85d8580dfcb876b56957ba5488222a4f7873)

[Compare Source](https://togithub.com/webpack/webpack/compare/v5.93.0...v5.94.0)

</details>

<details>
<summary>cloudflare/workers-sdk (wrangler)</summary>

### [`v3.72.2`](https://togithub.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3722)

[Compare Source](https://togithub.com/cloudflare/workers-sdk/compare/wrangler@3.72.1...wrangler@3.72.2)

##### Patch Changes

-   [#&#8203;6511](https://togithub.com/cloudflare/workers-sdk/pull/6511) [`e75c581`](e75c5812f5) Thanks [@&#8203;petebacondarwin](https://togithub.com/petebacondarwin)! - fix: allow Pages projects to use \`experimental:nodejs_compat_v2" flag

    Fixes [#&#8203;6288](https://togithub.com/cloudflare/workers-sdk/issues/6288)

-   Updated dependencies \[[`b0e2f0b`](b0e2f0bfc6), [`f5bde66`](f5bde66914)]:
    -   miniflare@3.20240821.0
    -   [@&#8203;cloudflare/workers-shared](https://togithub.com/cloudflare/workers-shared)[@&#8203;0](https://togithub.com/0).3.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-23 14:42:36 +00:00
darkskygit
0b3c7d1407 feat: update throttler (#7957) 2024-08-23 13:52:47 +00:00
renovate[bot]
49c8a25fce chore: bump up @blocksuite/icons version to v2.1.64 (#7945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-23 12:22:06 +08:00
JimmFly
920afa7bf1 feat(core): adjust share menu ui (#7931)
https://github.com/user-attachments/assets/6538c046-3872-4c98-a389-81b86a2978a4
2024-08-23 02:07:51 +00:00
darkskygit
b57388fd85 feat: block slides insert before image load finished (#7948) 2024-08-22 17:46:06 +00:00
pengx17
5e555b3807 fix(electron): adjust app-tabs-header styles (#7947)
fix AF-1171
2024-08-22 13:10:27 +00:00
darkskygit
3b727ef40a chore: revert breaking change deps (#7949) 2024-08-22 12:08:07 +00:00
EYHN
4bc4a58a30 feat(infra): add convenience api to get workspace from doc (#7934) 2024-08-22 11:22:04 +00:00
pengx17
2f02f0da2b fix(electron): should not send switchSplitView event when clicking on the active view (#7944) 2024-08-22 05:26:29 +00:00
pengx17
592150638e feat(electron): switch to next/previous tab with Ctrl+Tab/Ctrl+Shift+Tab (#7943)
fix PD-1569
2024-08-22 04:46:25 +00:00
JimmFly
20174b9cbe feat(core): add custom font family setting (#7924)
close AF-1255

https://github.com/user-attachments/assets/d44359b6-b75c-4883-a57b-1f226586feec
2024-08-22 04:24:44 +00:00
JimmFly
b333cde336 feat(core): init editor setting ui (#7878)
- init editor setting ui

https://github.com/user-attachments/assets/c54f5816-ef05-4ac0-b11a-8ab7159f928c
2024-08-22 04:24:41 +00:00
JimmFly
03c4d56a91 feat(component): add slider ui component (#7879)
![CleanShot 2024-08-15 at 14 27 07@2x](https://github.com/user-attachments/assets/50299f44-6446-4ec8-a097-7d456ff67f46)
2024-08-22 04:24:37 +00:00
renovate
2e2a3af967 chore: bump up all non-major dependencies (#7925)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence | Type | Update |
|---|---|---|---|---|---|---|---|
| [@aws-sdk/client-s3](https://togithub.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) ([source](https://togithub.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)) | [`3.633.0` -> `3.635.0`](https://renovatebot.com/diffs/npm/@aws-sdk%2fclient-s3/3.633.0/3.635.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@aws-sdk%2fclient-s3/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@aws-sdk%2fclient-s3/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@aws-sdk%2fclient-s3/3.633.0/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@aws-sdk%2fclient-s3/3.633.0/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@aws-sdk/client-s3](https://togithub.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) ([source](https://togithub.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)) | [`3.633.0` -> `3.635.0`](https://renovatebot.com/diffs/npm/@aws-sdk%2fclient-s3/3.633.0/3.635.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@aws-sdk%2fclient-s3/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@aws-sdk%2fclient-s3/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@aws-sdk%2fclient-s3/3.633.0/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@aws-sdk%2fclient-s3/3.633.0/3.635.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@chromatic-com/storybook](https://togithub.com/chromaui/addon-visual-tests) | [`1.6.1` -> `1.7.0`](https://renovatebot.com/diffs/npm/@chromatic-com%2fstorybook/1.6.1/1.7.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@chromatic-com%2fstorybook/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@chromatic-com%2fstorybook/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@chromatic-com%2fstorybook/1.6.1/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@chromatic-com%2fstorybook/1.6.1/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@emotion/react](https://togithub.com/emotion-js/emotion/tree/main#readme) ([source](https://togithub.com/emotion-js/emotion)) | [`11.13.0` -> `11.13.3`](https://renovatebot.com/diffs/npm/@emotion%2freact/11.13.0/11.13.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@emotion%2freact/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@emotion%2freact/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@emotion%2freact/11.13.0/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@emotion%2freact/11.13.0/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@emotion/react](https://togithub.com/emotion-js/emotion/tree/main#readme) ([source](https://togithub.com/emotion-js/emotion)) | [`11.13.0` -> `11.13.3`](https://renovatebot.com/diffs/npm/@emotion%2freact/11.13.0/11.13.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@emotion%2freact/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@emotion%2freact/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@emotion%2freact/11.13.0/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@emotion%2freact/11.13.0/11.13.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [@fal-ai/serverless-client](https://togithub.com/fal-ai/fal-js) ([source](https://togithub.com/fal-ai/fal-js/tree/HEAD/libs/client)) | [`^0.13.0` -> `^0.14.0`](https://renovatebot.com/diffs/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@fal-ai%2fserverless-client/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@fal-ai%2fserverless-client/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@napi-rs/cli](https://togithub.com/napi-rs/napi-rs) | [`3.0.0-alpha.60` -> `3.0.0-alpha.62`](https://renovatebot.com/diffs/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@napi-rs%2fcli/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@napi-rs%2fcli/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@playwright/test](https://playwright.dev) ([source](https://togithub.com/microsoft/playwright)) | [`=1.44.1` -> `=1.46.1`](https://renovatebot.com/diffs/npm/@playwright%2ftest/1.44.1/1.46.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@playwright%2ftest/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@playwright%2ftest/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@playwright%2ftest/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@playwright%2ftest/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@types/react](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react) ([source](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react)) | [`18.3.3` -> `18.3.4`](https://renovatebot.com/diffs/npm/@types%2freact/18.3.3/18.3.4) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2freact/18.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@types%2freact/18.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@types%2freact/18.3.3/18.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2freact/18.3.3/18.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@vanilla-extract/css](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/css)) | [`1.15.4` -> `1.15.5`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fcss/1.15.4/1.15.5) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fcss/1.15.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fcss/1.15.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fcss/1.15.4/1.15.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fcss/1.15.4/1.15.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@vanilla-extract/vite-plugin](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/vite-plugin)) | [`4.0.14` -> `4.0.15`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fvite-plugin/4.0.14/4.0.15) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fvite-plugin/4.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fvite-plugin/4.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fvite-plugin/4.0.14/4.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fvite-plugin/4.0.14/4.0.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@vanilla-extract/webpack-plugin](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/webpack-plugin)) | [`2.3.12` -> `2.3.13`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fwebpack-plugin/2.3.12/2.3.13) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fwebpack-plugin/2.3.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fwebpack-plugin/2.3.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fwebpack-plugin/2.3.12/2.3.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fwebpack-plugin/2.3.12/2.3.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [core-js](https://togithub.com/zloirock/core-js) ([source](https://togithub.com/zloirock/core-js/tree/HEAD/packages/core-js)) | [`3.38.0` -> `3.38.1`](https://renovatebot.com/diffs/npm/core-js/3.38.0/3.38.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/core-js/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/core-js/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/core-js/3.38.0/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/core-js/3.38.0/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [core-js](https://togithub.com/zloirock/core-js) ([source](https://togithub.com/zloirock/core-js/tree/HEAD/packages/core-js)) | [`3.38.0` -> `3.38.1`](https://renovatebot.com/diffs/npm/core-js/3.38.0/3.38.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/core-js/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/core-js/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/core-js/3.38.0/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/core-js/3.38.0/3.38.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [dayjs](https://day.js.org) ([source](https://togithub.com/iamkun/dayjs)) | [`1.11.12` -> `1.11.13`](https://renovatebot.com/diffs/npm/dayjs/1.11.12/1.11.13) | [![age](https://developer.mend.io/api/mc/badges/age/npm/dayjs/1.11.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/dayjs/1.11.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/dayjs/1.11.12/1.11.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dayjs/1.11.12/1.11.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [electron](https://togithub.com/electron/electron) | [`32.0.0` -> `32.0.1`](https://renovatebot.com/diffs/npm/electron/32.0.0/32.0.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/electron/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/electron/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/electron/32.0.0/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/electron/32.0.0/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [embla-carousel-react](https://www.embla-carousel.com) ([source](https://togithub.com/davidjerleke/embla-carousel)) | [`8.1.8` -> `8.2.0`](https://renovatebot.com/diffs/npm/embla-carousel-react/8.1.8/8.2.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/embla-carousel-react/8.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/embla-carousel-react/8.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/embla-carousel-react/8.1.8/8.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/embla-carousel-react/8.1.8/8.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [husky](https://togithub.com/typicode/husky) | [`9.1.4` -> `9.1.5`](https://renovatebot.com/diffs/npm/husky/9.1.4/9.1.5) | [![age](https://developer.mend.io/api/mc/badges/age/npm/husky/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/husky/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/husky/9.1.4/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/husky/9.1.4/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [jotai-scope](https://togithub.com/jotaijs/jotai-scope) | [`0.7.1` -> `0.7.2`](https://renovatebot.com/diffs/npm/jotai-scope/0.7.1/0.7.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai-scope/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai-scope/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai-scope/0.7.1/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai-scope/0.7.1/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [lucide-react](https://lucide.dev) ([source](https://togithub.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react)) | [`^0.408.0` -> `^0.429.0`](https://renovatebot.com/diffs/npm/lucide-react/0.408.0/0.429.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-react/0.429.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lucide-react/0.429.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lucide-react/0.408.0/0.429.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-react/0.408.0/0.429.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [napi](https://togithub.com/napi-rs/napi-rs) | `3.0.0-alpha.7` -> `3.0.0-alpha.8` | [![age](https://developer.mend.io/api/mc/badges/age/crate/napi/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi/3.0.0-alpha.7/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi/3.0.0-alpha.7/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [napi-derive](https://togithub.com/napi-rs/napi-rs) | `3.0.0-alpha.6` -> `3.0.0-alpha.7` | [![age](https://developer.mend.io/api/mc/badges/age/crate/napi-derive/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi-derive/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi-derive/3.0.0-alpha.6/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi-derive/3.0.0-alpha.6/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [nestjs-throttler-storage-redis](https://togithub.com/kkoomen/nestjs-throttler-storage-redis) | [`^0.4.1` -> `^0.5.0`](https://renovatebot.com/diffs/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nestjs-throttler-storage-redis/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nestjs-throttler-storage-redis/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [node](https://nodejs.org) ([source](https://togithub.com/nodejs/node)) | `20.15.1` -> `20.16.0` | [![age](https://developer.mend.io/api/mc/badges/age/node-version/node/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/node-version/node/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/node-version/node/v20.15.1/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/node-version/node/v20.15.1/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |  | minor |
| openresty/openresty | `1.25.3.1-0-buster` -> `1.25.3.2-0-buster` | [![age](https://developer.mend.io/api/mc/badges/age/docker/openresty%2fopenresty/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/docker/openresty%2fopenresty/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/docker/openresty%2fopenresty/1.25.3.1/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/docker/openresty%2fopenresty/1.25.3.1/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | final | patch |
| [playwright](https://playwright.dev) ([source](https://togithub.com/microsoft/playwright)) | [`=1.44.1` -> `=1.46.1`](https://renovatebot.com/diffs/npm/playwright/1.44.1/1.46.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/playwright/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/playwright/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/playwright/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/playwright/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [react-refresh](https://reactjs.org/) ([source](https://togithub.com/facebook/react/tree/HEAD/packages/react)) | [`^0.10.0` -> `^0.14.0`](https://renovatebot.com/diffs/npm/react-refresh/0.10.0/0.14.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-refresh/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-refresh/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-refresh/0.10.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-refresh/0.10.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [serde](https://serde.rs) ([source](https://togithub.com/serde-rs/serde)) | `1.0.204` -> `1.0.208` | [![age](https://developer.mend.io/api/mc/badges/age/crate/serde/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/serde/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/serde/1.0.204/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/serde/1.0.204/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [serde_json](https://togithub.com/serde-rs/json) | `1.0.120` -> `1.0.125` | [![age](https://developer.mend.io/api/mc/badges/age/crate/serde_json/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/serde_json/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/serde_json/1.0.120/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/serde_json/1.0.120/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [storybook-dark-mode](https://togithub.com/hipstersmoothie/storybook-dark-mode) | [`4.0.1` -> `4.0.2`](https://renovatebot.com/diffs/npm/storybook-dark-mode/4.0.1/4.0.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/storybook-dark-mode/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/storybook-dark-mode/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/storybook-dark-mode/4.0.1/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/storybook-dark-mode/4.0.1/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [tokio](https://tokio.rs) ([source](https://togithub.com/tokio-rs/tokio)) | `1.38.0` -> `1.39.3` | [![age](https://developer.mend.io/api/mc/badges/age/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dev-dependencies | minor |
| [tokio](https://tokio.rs) ([source](https://togithub.com/tokio-rs/tokio)) | `1.38.0` -> `1.39.3` | [![age](https://developer.mend.io/api/mc/badges/age/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | minor |
| [vite](https://vitejs.dev) ([source](https://togithub.com/vitejs/vite/tree/HEAD/packages/vite)) | [`5.4.1` -> `5.4.2`](https://renovatebot.com/diffs/npm/vite/5.4.1/5.4.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/vite/5.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite/5.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite/5.4.1/5.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/5.4.1/5.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [vite-plugin-dts](https://togithub.com/qmhc/vite-plugin-dts) | [`4.0.2` -> `4.0.3`](https://renovatebot.com/diffs/npm/vite-plugin-dts/4.0.2/4.0.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/vite-plugin-dts/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite-plugin-dts/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite-plugin-dts/4.0.2/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite-plugin-dts/4.0.2/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [wrangler](https://togithub.com/cloudflare/workers-sdk) ([source](https://togithub.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler)) | [`3.72.0` -> `3.72.1`](https://renovatebot.com/diffs/npm/wrangler/3.72.0/3.72.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/3.72.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/wrangler/3.72.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/wrangler/3.72.0/3.72.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/3.72.0/3.72.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |

---

### Release Notes

<details>
<summary>aws/aws-sdk-js-v3 (@&#8203;aws-sdk/client-s3)</summary>

### [`v3.635.0`](https://togithub.com/aws/aws-sdk-js-v3/blob/HEAD/clients/client-s3/CHANGELOG.md#36350-2024-08-20)

[Compare Source](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.633.0...v3.635.0)

##### Features

-   **client-s3:** Amazon Simple Storage Service / Features : Add support for conditional writes for PutObject and CompleteMultipartUpload APIs. ([b474584](b474584f2c))
-   **codegen:** add Smithy RPCv2 CBOR to list of protocols ([#&#8203;6096](https://togithub.com/aws/aws-sdk-js-v3/issues/6096)) ([5154d4f](5154d4f19b))

</details>

<details>
<summary>chromaui/addon-visual-tests (@&#8203;chromatic-com/storybook)</summary>

### [`v1.7.0`](https://togithub.com/chromaui/addon-visual-tests/blob/HEAD/CHANGELOG.md#v170-Tue-Aug-20-2024)

[Compare Source](https://togithub.com/chromaui/addon-visual-tests/compare/v1.6.1...v1.7.0)

##### 🚀 Enhancement

-   Update story status reporting for Storybook 8.3 and use new `SET_FILTER` event [#&#8203;332](https://togithub.com/chromaui/addon-visual-tests/pull/332) ([@&#8203;ghengeveld](https://togithub.com/ghengeveld))

##### Authors: 1

-   Gert Hengeveld ([@&#8203;ghengeveld](https://togithub.com/ghengeveld))

***

</details>

<details>
<summary>emotion-js/emotion (@&#8203;emotion/react)</summary>

### [`v11.13.3`](https://togithub.com/emotion-js/emotion/compare/@emotion/react@11.13.0...3f468846855ed1c6092922a6317a6f5df0ba8dcc)

[Compare Source](https://togithub.com/emotion-js/emotion/compare/@emotion/react@11.13.0...@emotion/react@11.13.3)

</details>

<details>
<summary>fal-ai/fal-js (@&#8203;fal-ai/serverless-client)</summary>

### [`v0.14.2`](c3a3c3d21a...b3ab5f0e15)

[Compare Source](c3a3c3d21a...b3ab5f0e15)

### [`v0.14.1`](6edbf2948d...c3a3c3d21a)

[Compare Source](6edbf2948d...c3a3c3d21a)

### [`v0.14.0`](cf300e9cc0...6edbf2948d)

[Compare Source](cf300e9cc0...6edbf2948d)

</details>

<details>
<summary>napi-rs/napi-rs (@&#8203;napi-rs/cli)</summary>

### [`v3.0.0-alpha.62`](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.61...@napi-rs/cli@3.0.0-alpha.62)

[Compare Source](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.61...@napi-rs/cli@3.0.0-alpha.62)

### [`v3.0.0-alpha.61`](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.60...@napi-rs/cli@3.0.0-alpha.61)

[Compare Source](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.60...@napi-rs/cli@3.0.0-alpha.61)

</details>

<details>
<summary>microsoft/playwright (@&#8203;playwright/test)</summary>

### [`v1.46.1`](https://togithub.com/microsoft/playwright/compare/v1.46.0...e1c861cfa7a6caf3c5b798786b1e6298c4f3cf31)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.46.0...v1.46.1)

### [`v1.46.0`](https://togithub.com/microsoft/playwright/compare/v1.45.3...99a36310570617222290c09b96a2026beb8b00f9)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.3...v1.46.0)

### [`v1.45.3`](https://togithub.com/microsoft/playwright/compare/v1.45.2...0e130fa8edaf85765c4a5a86bded0e6d33bfd7c2)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.2...v1.45.3)

### [`v1.45.2`](https://togithub.com/microsoft/playwright/compare/v1.45.1...d8a5f3b33193e413b404ff4aa1f71e859d8f1b6b)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.1...v1.45.2)

### [`v1.45.1`](https://togithub.com/microsoft/playwright/compare/v1.45.0...e8989f83d9801cdaadc3803b5341c601c9593947)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.0...v1.45.1)

### [`v1.45.0`](https://togithub.com/microsoft/playwright/compare/v1.44.1...4f3f6eecae490af444dd9298c9eaeb0c596915b7)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.44.1...v1.45.0)

</details>

<details>
<summary>vanilla-extract-css/vanilla-extract (@&#8203;vanilla-extract/css)</summary>

### [`v1.15.5`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/css/CHANGELOG.md#1155)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/css@1.15.4...@vanilla-extract/css@1.15.5)

##### Patch Changes

-   [#&#8203;1466](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1466) [`6432199fa0717f424fb3f45fbe36410b03b01c1c`](6432199fa0) Thanks [@&#8203;askoufis](https://togithub.com/askoufis)! - Speed up dev prefix generation for long file paths

</details>

<details>
<summary>vanilla-extract-css/vanilla-extract (@&#8203;vanilla-extract/vite-plugin)</summary>

### [`v4.0.15`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/vite-plugin/CHANGELOG.md#4015)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/vite-plugin@4.0.14...@vanilla-extract/vite-plugin@4.0.15)

##### Patch Changes

-   Updated dependencies \[[`96dd466127374b21ad7e48e5dd168a03a96af047`](96dd466127)]:
    -   [@&#8203;vanilla-extract/integration](https://togithub.com/vanilla-extract/integration)[@&#8203;7](https://togithub.com/7).1.9

</details>

<details>
<summary>vanilla-extract-css/vanilla-extract (@&#8203;vanilla-extract/webpack-plugin)</summary>

### [`v2.3.13`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/webpack-plugin/CHANGELOG.md#2313)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/webpack-plugin@2.3.12...@vanilla-extract/webpack-plugin@2.3.13)

##### Patch Changes

-   Updated dependencies \[[`96dd466127374b21ad7e48e5dd168a03a96af047`](96dd466127)]:
    -   [@&#8203;vanilla-extract/integration](https://togithub.com/vanilla-extract/integration)[@&#8203;7](https://togithub.com/7).1.9

</details>

<details>
<summary>zloirock/core-js (core-js)</summary>

### [`v3.38.1`](https://togithub.com/zloirock/core-js/blob/HEAD/CHANGELOG.md#3381---20240820)

[Compare Source](https://togithub.com/zloirock/core-js/compare/v3.38.0...v3.38.1)

-   Changes [v3.38.0...v3.38.1](https://togithub.com/zloirock/core-js/compare/v3.38.0...v3.38.1)
-   Fixed some cases of `URLSearchParams` percent decoding, [#&#8203;1357](https://togithub.com/zloirock/core-js/issues/1357), [#&#8203;1361](https://togithub.com/zloirock/core-js/pull/1361), thanks [**@&#8203;slowcheetah**](https://togithub.com/slowcheetah)
-   Some stylistic changes and minor optimizations
-   Compat data improvements:
    -   [`Iterator` helpers proposal](https://togithub.com/tc39/proposal-iterator-helpers) methods marked as [shipped from FF131](https://bugzilla.mozilla.org/show_bug.cgi?id=1896390)
    -   [`Math.f16round` and `DataView.prototype.{ getFloat16, setFloat16 }`](https://togithub.com/tc39/proposal-float16array) marked as shipped from Bun 1.1.23
    -   [`RegExp.escape`](https://togithub.com/tc39/proposal-regex-escaping) marked as shipped from Bun 1.1.22
    -   [`Promise.try`](https://togithub.com/tc39/proposal-promise-try) marked as shipped from Bun 1.1.22
    -   [`Uint8Array` to / from base64 and hex proposal](https://togithub.com/tc39/proposal-arraybuffer-base64) methods marked as shipped from Bun 1.1.22
    -   Added Hermes 0.13 compat data, similar to React Native 0.75 Hermes
    -   Added Opera Android 84 compat data mapping

</details>

<details>
<summary>iamkun/dayjs (dayjs)</summary>

### [`v1.11.13`](https://togithub.com/iamkun/dayjs/compare/v1.11.12...93c8fd0f807b8a8252f4cd65083bb1d6a49b90e7)

[Compare Source](https://togithub.com/iamkun/dayjs/compare/v1.11.12...v1.11.13)

</details>

<details>
<summary>electron/electron (electron)</summary>

### [`v32.0.1`](https://togithub.com/electron/electron/compare/v32.0.0...v32.0.1)

[Compare Source](https://togithub.com/electron/electron/compare/v32.0.0...v32.0.1)

</details>

<details>
<summary>davidjerleke/embla-carousel (embla-carousel-react)</summary>

### [`v8.2.0`](https://togithub.com/davidjerleke/embla-carousel/compare/v8.1.8...6baf1555c6f68e88a7f785213ecf363f447a8b2f)

[Compare Source](https://togithub.com/davidjerleke/embla-carousel/compare/v8.1.8...v8.2.0)

</details>

<details>
<summary>typicode/husky (husky)</summary>

### [`v9.1.5`](https://togithub.com/typicode/husky/compare/v9.1.4...2fee8d212c601942ad146ea9209f15c20a07fb6d)

[Compare Source](https://togithub.com/typicode/husky/compare/v9.1.4...v9.1.5)

</details>

<details>
<summary>jotaijs/jotai-scope (jotai-scope)</summary>

### [`v0.7.2`](https://togithub.com/jotaijs/jotai-scope/compare/v0.7.1...v0.7.2)

[Compare Source](https://togithub.com/jotaijs/jotai-scope/compare/v0.7.1...v0.7.2)

</details>

<details>
<summary>lucide-icons/lucide (lucide-react)</summary>

### [`v0.429.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.429.0): New icons 0.429.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.428.0...0.429.0)

#### Modified Icons 🔨

-   `message-square-dashed` ([#&#8203;2374](https://togithub.com/lucide-icons/lucide/issues/2374)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `stethoscope` ([#&#8203;2379](https://togithub.com/lucide-icons/lucide/issues/2379)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.428.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.428.0): New icons 0.428.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.427.0...0.428.0)

#### New icons 🎨

-   `tickets-plane` ([#&#8203;2196](https://togithub.com/lucide-icons/lucide/issues/2196)) by [@&#8203;jguddas](https://togithub.com/jguddas)

#### Modified Icons 🔨

-   `folder-search` ([#&#8203;2354](https://togithub.com/lucide-icons/lucide/issues/2354)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.427.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.427.0): New icons 0.427.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.426.0...0.427.0)

#### New icons 🎨

-   `binoculars` ([#&#8203;2207](https://togithub.com/lucide-icons/lucide/issues/2207)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `tickets` ([#&#8203;2335](https://togithub.com/lucide-icons/lucide/issues/2335)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.426.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.426.0): New icons 0.426.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.425.0...0.426.0)

#### New icons 🎨

-   `chevrons-left-right-ellipsis` ([#&#8203;2120](https://togithub.com/lucide-icons/lucide/issues/2120)) by [@&#8203;ericfennis](https://togithub.com/ericfennis)
-   `ethernet-port` ([#&#8203;2120](https://togithub.com/lucide-icons/lucide/issues/2120)) by [@&#8203;ericfennis](https://togithub.com/ericfennis)

#### Modified Icons 🔨

-   `cigarette-off` ([#&#8203;2282](https://togithub.com/lucide-icons/lucide/issues/2282)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `cigarette` ([#&#8203;2282](https://togithub.com/lucide-icons/lucide/issues/2282)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.425.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.425.0): New icons 0.425.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.424.0...0.425.0)

#### New icons 🎨

-   `bandage` ([#&#8203;2341](https://togithub.com/lucide-icons/lucide/issues/2341)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `table-of-contents` ([#&#8203;2348](https://togithub.com/lucide-icons/lucide/issues/2348)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `mouse-pointer-2` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `mouse-pointer-ban` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `mouse-pointer-click` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `mouse-pointer` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `square-dashed-mouse-pointer` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `square-mouse-pointer` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.424.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.424.0): New icons 0.424.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.423.0...0.424.0)

#### New icons 🎨

-   `map-pin-house` ([#&#8203;2337](https://togithub.com/lucide-icons/lucide/issues/2337)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `replace-all` ([#&#8203;2333](https://togithub.com/lucide-icons/lucide/issues/2333)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `replace` ([#&#8203;2333](https://togithub.com/lucide-icons/lucide/issues/2333)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.423.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.423.0): New icons 0.423.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.422.0...0.423.0)

#### New icons 🎨

-   `amphora` ([#&#8203;1926](https://togithub.com/lucide-icons/lucide/issues/1926)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.422.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.422.0): New icons 0.422.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.421.0...0.422.0)

#### Modified Icons 🔨

-   `skull` ([#&#8203;2197](https://togithub.com/lucide-icons/lucide/issues/2197)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.421.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.421.0): New icons 0.421.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.420.0...0.421.0)

#### New icons 🎨

-   `microchip` ([#&#8203;1982](https://togithub.com/lucide-icons/lucide/issues/1982)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `circle-check-big` ([#&#8203;2330](https://togithub.com/lucide-icons/lucide/issues/2330)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `cloud-download` ([#&#8203;2355](https://togithub.com/lucide-icons/lucide/issues/2355)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `pentagon` ([#&#8203;1918](https://togithub.com/lucide-icons/lucide/issues/1918)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `square-check-big` ([#&#8203;2331](https://togithub.com/lucide-icons/lucide/issues/2331)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.420.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.420.0): New icons 0.420.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.419.0...0.420.0)

#### New icons 🎨

-   `omega` ([#&#8203;2347](https://togithub.com/lucide-icons/lucide/issues/2347)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `calendar-search` ([#&#8203;2351](https://togithub.com/lucide-icons/lucide/issues/2351)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `cloud-upload` ([#&#8203;2352](https://togithub.com/lucide-icons/lucide/issues/2352)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.419.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.419.0): New icons 0.419.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.418.0...0.419.0)

#### New icons 🎨

-   `circle-fading-arrow-up` ([#&#8203;2287](https://togithub.com/lucide-icons/lucide/issues/2287)) by [@&#8203;mosch](https://togithub.com/mosch)

### [`v0.418.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.418.0): New icons 0.418.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.417.0...0.418.0)

#### New icons 🎨

-   `id-card` ([#&#8203;1296](https://togithub.com/lucide-icons/lucide/issues/1296)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.417.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.417.0): New icons 0.417.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.416.0...0.417.0)

#### Modified Icons 🔨

-   `chart-column-increasing` ([#&#8203;2334](https://togithub.com/lucide-icons/lucide/issues/2334)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.416.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.416.0): New icons 0.416.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.415.0...0.416.0)

#### New icons 🎨

-   `map-pin-check-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-check` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-minus-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-minus` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-plus-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-plus` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-x-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-x` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `map-pin-off` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pinned` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.415.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.415.0): New icons 0.415.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.414.0...0.415.0)

#### New icons 🎨

-   `square-square` ([#&#8203;2241](https://togithub.com/lucide-icons/lucide/issues/2241)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.414.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.414.0): New icons 0.414.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.413.0...0.414.0)

#### New icons 🎨

-   `chart-area` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-bar-decreasing` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-bar-increasing` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-bar-stacked` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-column-big` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-column-increasing` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-column-stacked` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-network` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-no-axes-combined` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-spline` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.413.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.413.0): New icons 0.413.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.412.0...0.413.0)

#### New icons 🎨

-   `dam` ([#&#8203;2233](https://togithub.com/lucide-icons/lucide/issues/2233)) by [@&#8203;AndreasSas](https://togithub.com/AndreasSas)

#### Modified Icons 🔨

-   `dog` ([#&#8203;2249](https://togithub.com/lucide-icons/lucide/issues/2249)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `key-square` ([#&#8203;2277](https://togithub.com/lucide-icons/lucide/issues/2277)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.412.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.412.0): New icons 0.412.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.411.0...0.412.0)

#### New icons 🎨

-   `letter-text` ([#&#8203;2252](https://togithub.com/lucide-icons/lucide/issues/2252)) by [@&#8203;GRA0007](https://togithub.com/GRA0007)

### [`v0.411.0`](https://togithub.com/lucide-icons/lucide/compare/0.410.0...0.411.0)

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.410.0...0.411.0)

### [`v0.410.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.410.0): New icons 0.410.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.409.0...0.410.0)

#### New icons 🎨

-   `philippine-peso` ([#&#8203;2231](https://togithub.com/lucide-icons/lucide/issues/2231)) by [@&#8203;kasutu](https://togithub.com/kasutu)

#### Modified Icons 🔨

-   `ribbon` ([#&#8203;2271](https://togithub.com/lucide-icons/lucide/issues/2271)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.409.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.409.0): New icons 0.409.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.408.0...0.409.0)

#### Modified Icons 🔨

-   `calendar-minus` ([#&#8203;2265](https://togithub.com/lucide-icons/lucide/issues/2265)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `eye-off` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `eye` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `image-plus` ([#&#8203;2321](https://togithub.com/lucide-icons/lucide/issues/2321)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `scan-eye` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `view` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

</details>

<details>
<summary>nodejs/node (node)</summary>

### [`v20.16.0`](https://togithub.com/nodejs/node/compare/v20.15.1...v20.16.0)

[Compare Source](https://togithub.com/nodejs/node/compare/v20.15.1...v20.16.0)

</details>

<details>
<summary>facebook/react (react-refresh)</summary>

### [`v0.14.2`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#0142-November-2-2015)

[Compare Source](https://togithub.com/facebook/react/compare/v0.14.1...v0.14.2)

##### React DOM

-   Fixed bug with development build preventing events from firing in some versions of Internet Explorer & Edge
-   Fixed bug with development build when using es5-sham in older versions of Internet Explorer
-   Added support for `integrity` attribute
-   Fixed bug resulting in `children` prop being coerced to a string for custom elements, which was not the desired behavior
-   Moved `react` from `dependencies` to `peerDependencies` to match expectations and align with `react-addons-*` packages

### [`v0.14.1`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#01410-October-14-2020)

[Compare Source](https://togithub.com/facebook/react/compare/v0.14.0...v0.14.1)

##### React

-   Backport support for the [new JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) to 0.14.x. ([@&#8203;lunaruan](https://togithub.com/lunaruan) in [#&#8203;18299](https://togithub.com/facebook/react/pull/18299) and [@&#8203;gaearon](https://togithub.com/gaearon) in [#&#8203;20024](https://togithub.com/facebook/react/pull/20024))

### [`v0.14.0`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#0140-October-7-2015)

[Compare Source](https://togithub.com/facebook/react/compare/v0.13.0...v0.14.0)

##### Major changes

-   Split the main `react` package into two: `react` and `react-dom`.  This paves the way to writing components that can be shared between the web version of React and React Native.  This means you will need to include both files and some functions have been moved from `React` to `ReactDOM`.
-   Addons have been moved to separate packages (`react-addons-clone-with-props`, `react-addons-create-fragment`, `react-addons-css-transition-group`, `react-addons-linked-state-mixin`, `react-addons-perf`, `react-addons-pure-render-mixin`, `react-addons-shallow-compare`, `react-addons-test-utils`, `react-addons-transition-group`, `react-addons-update`, `ReactDOM.unstable_batchedUpdates`).
-   Stateless functional components - React components were previously created using React.createClass or using ES6 classes.  This release adds a [new syntax](https://reactjs.org/docs/reusable-components.html#stateless-functions) where a user defines a single [stateless render function](https://reactjs.org/docs/reusable-components.html#stateless-functions) (with one parameter: `props`) which returns a JSX element, and this function may be used as a component.
-   Refs to DOM components as the DOM node itself. Previously the only useful thing you can do with a DOM component is call `getDOMNode()` to get the underlying DOM node. Starting with this release, a ref to a DOM component *is* the actual DOM node. **Note that refs to custom (user-defined) components work exactly as before; only the built-in DOM components are affected by this change.**

##### Breaking changes

-   `React.initializeTouchEvents` is no longer necessary and has been removed completely. Touch events now work automatically.
-   Add-Ons: Due to the DOM node refs change mentioned above, `TestUtils.findAllInRenderedTree` and related helpers are no longer able to take a DOM component, only a custom component.
-   The `props` object is now frozen, so mutating props after creating a component element is no longer supported. In most cases, [`React.cloneElement`](https://reactjs.org/docs/react-api.html#cloneelement) should be used instead. This change makes your components easier to reason about and enables the compiler optimizations mentioned above.
-   Plain objects are no longer supported as React children; arrays should be used instead. You can use the [`createFragment`](https://reactjs.org/docs/create-fragment.html) helper to migrate, which now returns an array.
-   Add-Ons: `classSet` has been removed. Use [classnames](https://togithub.com/JedWatson/classnames) instead.
-   Web components (custom elements) now use native property names.  Eg: `class` instead of `className`.

##### Deprecations

-   `this.getDOMNode()` is now deprecated and `ReactDOM.findDOMNode(this)` can be used instead. Note that in the common case, `findDOMNode` is now unnecessary since a ref to the DOM component is now the actual DOM node.
-   `setProps` and `replaceProps` are now deprecated. Instead, call ReactDOM.render again at the top level with the new props.
-   ES6 component classes must now extend `React.Component` in order to enable stateless function components. The [ES3 module pattern](https://reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html#other-languages) will continue to work.
-   Reusing and mutating a `style` object between renders has been deprecated. This mirrors our change to freeze the `props` object.
-   Add-Ons: `cloneWithProps` is now deprecated. Use [`React.cloneElement`](https://reactjs.org/docs/react-api.html#cloneelement) instead (unlike `cloneW

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-22 04:12:23 +00:00
pengx17
5acf1b5309 feat: init mobile entry (#7905) 2024-08-21 13:17:35 +00:00
EYHN
3db95bafa2 feat(core): use new print pdf api (#7932) 2024-08-21 10:06:22 +00:00
renovate
cf086e4018 chore: bump up @keyv/redis version to v3 (#7935)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@keyv/redis](https://togithub.com/jaredwray/keyv) | [`^2.8.4` -> `^3.0.0`](https://renovatebot.com/diffs/npm/@keyv%2fredis/2.8.5/3.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@keyv%2fredis/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@keyv%2fredis/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@keyv%2fredis/2.8.5/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@keyv%2fredis/2.8.5/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-21 09:37:17 +00:00
renovate
483fcfb2c7 chore: bump up @blocksuite/icons version to v2.1.63 (#7845)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@blocksuite/icons](https://togithub.com/toeverything/icons) | [`2.1.62` -> `2.1.63`](https://renovatebot.com/diffs/npm/@blocksuite%2ficons/2.1.62/2.1.63) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@blocksuite%2ficons/2.1.63?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@blocksuite%2ficons/2.1.63?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@blocksuite%2ficons/2.1.62/2.1.63?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@blocksuite%2ficons/2.1.62/2.1.63?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>toeverything/icons (@&#8203;blocksuite/icons)</summary>

### [`v2.1.63`](dce7e696b3...d63ff94c9f)

[Compare Source](dce7e696b3...d63ff94c9f)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yMC4xIiwidXBkYXRlZEluVmVyIjoiMzguMjAuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-21 09:20:14 +00:00
renovate
b6a3697793 chore: bump up oxlint version to v0.7.2 (#7885)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [oxlint](https://oxc.rs) ([source](https://togithub.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`0.7.1` -> `0.7.2`](https://renovatebot.com/diffs/npm/oxlint/0.7.1/0.7.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxlint/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxlint/0.7.1/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/0.7.1/0.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v0.7.2`](https://togithub.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#072---2024-08-15)

[Compare Source](972492cc4d...b3e189764f)

##### Features

-   [`4d28d03`](https://togithub.com/oxc-project/oxc/commit/4d28d03) task/website: Support render `subschemas.all_of` ([#&#8203;4800](https://togithub.com/oxc-project/oxc/issues/4800)) (mysteryven)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-21 09:05:36 +00:00
Cats Juice
23b0db36b9 feat(component): mobile menu support (#7892) 2024-08-21 09:05:05 +00:00
renovate
182b2fd62d chore: bump up electron version to v32 (#7927)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [electron](https://togithub.com/electron/electron) | [`^31.0.0` -> `^32.0.0`](https://renovatebot.com/diffs/npm/electron/31.4.0/32.0.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/electron/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/electron/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/electron/31.4.0/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/electron/31.4.0/32.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>electron/electron (electron)</summary>

### [`v32.0.1`](https://togithub.com/electron/electron/compare/v32.0.0...v32.0.1)

[Compare Source](https://togithub.com/electron/electron/compare/v32.0.0...v32.0.1)

### [`v32.0.0`](https://togithub.com/electron/electron/releases/tag/v32.0.0): electron v32.0.0

[Compare Source](https://togithub.com/electron/electron/compare/v31.4.0...v32.0.0)

### Release Notes for v32.0.0

#### Features

-   Added support for responding to auth requests initiated from utility process via `app#login` event. [#&#8203;43317](https://togithub.com/electron/electron/pull/43317)
-   Enabled zstd compression in net http requests. [#&#8203;43300](https://togithub.com/electron/electron/pull/43300) <span style="font-size:small;">(Also in [31](https://togithub.com/electron/electron/pull/43301))</span>

#### Other Changes

-   Updated Chromium to 128.0.6613.36. [#&#8203;43328](https://togithub.com/electron/electron/pull/43328)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-21 08:38:21 +00:00
CatsJuice
a43c34feec fix(core): onboarding switch active state is invisible (#7928)
close AF-1262
2024-08-21 06:17:31 +00:00
CatsJuice
c9a1a8c3b2 feat(component): access enviroment in components storybook (#7891)
close AF-1247
2024-08-21 06:04:39 +00:00
CatsJuice
a49b92e4c8 feat(core): adjust sidebar style, add github & learn more (#7864)
close AF-1202
2024-08-21 05:48:04 +00:00
EYHN
cfe0677b84 refactor(core): adapt to new sync api (#7929) 2024-08-21 05:30:34 +00:00
forehalo
b6f46e01c5 test(server): space adapters (#7903) 2024-08-21 05:30:30 +00:00
forehalo
e20bdbf925 feat(server): make server storage adapters (#7902) 2024-08-21 05:30:26 +00:00
forehalo
6f9f579e5d feat(server): make an abstraction for ydoc storage (#7901) 2024-08-21 05:30:23 +00:00
forehalo
682a01e441 feat(server): make a singleton global mutex service (#7900) 2024-08-21 05:30:19 +00:00
Saul-Mirone
6b0c398ae5 chore: bump bs (#7914) 2024-08-20 00:24:01 +00:00
renovate
152815c175 chore: bump up all non-major dependencies (#7921)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence | Type | Update |
|---|---|---|---|---|---|---|---|
| [@fal-ai/serverless-client](https://togithub.com/fal-ai/fal-js) ([source](https://togithub.com/fal-ai/fal-js/tree/HEAD/libs/client)) | [`^0.13.0` -> `^0.14.0`](https://renovatebot.com/diffs/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@fal-ai%2fserverless-client/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@fal-ai%2fserverless-client/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@napi-rs/cli](https://togithub.com/napi-rs/napi-rs) | [`3.0.0-alpha.60` -> `3.0.0-alpha.62`](https://renovatebot.com/diffs/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@napi-rs%2fcli/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@napi-rs%2fcli/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@nx/vite](https://nx.dev) ([source](https://togithub.com/nrwl/nx/tree/HEAD/packages/vite)) | [`19.6.0` -> `19.6.1`](https://renovatebot.com/diffs/npm/@nx%2fvite/19.6.0/19.6.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nx%2fvite/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nx%2fvite/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nx%2fvite/19.6.0/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nx%2fvite/19.6.0/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@playwright/test](https://playwright.dev) ([source](https://togithub.com/microsoft/playwright)) | [`=1.44.1` -> `=1.46.1`](https://renovatebot.com/diffs/npm/@playwright%2ftest/1.44.1/1.46.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@playwright%2ftest/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@playwright%2ftest/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@playwright%2ftest/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@playwright%2ftest/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@swc/core](https://swc.rs) ([source](https://togithub.com/swc-project/swc)) | [`1.7.12` -> `1.7.14`](https://renovatebot.com/diffs/npm/@swc%2fcore/1.7.12/1.7.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@swc%2fcore/1.7.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@swc%2fcore/1.7.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@swc%2fcore/1.7.12/1.7.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@swc%2fcore/1.7.12/1.7.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [i18next](https://www.i18next.com) ([source](https://togithub.com/i18next/i18next)) | [`23.13.0` -> `23.14.0`](https://renovatebot.com/diffs/npm/i18next/23.13.0/23.14.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/i18next/23.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/i18next/23.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/i18next/23.13.0/23.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/i18next/23.13.0/23.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [lucide-react](https://lucide.dev) ([source](https://togithub.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react)) | [`^0.408.0` -> `^0.428.0`](https://renovatebot.com/diffs/npm/lucide-react/0.408.0/0.428.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-react/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lucide-react/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lucide-react/0.408.0/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-react/0.408.0/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [mini-css-extract-plugin](https://togithub.com/webpack-contrib/mini-css-extract-plugin) | [`2.9.0` -> `2.9.1`](https://renovatebot.com/diffs/npm/mini-css-extract-plugin/2.9.0/2.9.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/mini-css-extract-plugin/2.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/mini-css-extract-plugin/2.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/mini-css-extract-plugin/2.9.0/2.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mini-css-extract-plugin/2.9.0/2.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [napi](https://togithub.com/napi-rs/napi-rs) | `3.0.0-alpha.7` -> `3.0.0-alpha.8` | [![age](https://developer.mend.io/api/mc/badges/age/crate/napi/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi/3.0.0-alpha.7/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi/3.0.0-alpha.7/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [napi-derive](https://togithub.com/napi-rs/napi-rs) | `3.0.0-alpha.6` -> `3.0.0-alpha.7` | [![age](https://developer.mend.io/api/mc/badges/age/crate/napi-derive/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi-derive/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi-derive/3.0.0-alpha.6/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi-derive/3.0.0-alpha.6/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [nestjs-throttler-storage-redis](https://togithub.com/kkoomen/nestjs-throttler-storage-redis) | [`^0.4.1` -> `^0.5.0`](https://renovatebot.com/diffs/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nestjs-throttler-storage-redis/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nestjs-throttler-storage-redis/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [node](https://nodejs.org) ([source](https://togithub.com/nodejs/node)) | `20.15.1` -> `20.16.0` | [![age](https://developer.mend.io/api/mc/badges/age/node-version/node/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/node-version/node/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/node-version/node/v20.15.1/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/node-version/node/v20.15.1/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |  | minor |
| [nx](https://nx.dev) ([source](https://togithub.com/nrwl/nx/tree/HEAD/packages/nx)) | [`19.6.0` -> `19.6.1`](https://renovatebot.com/diffs/npm/nx/19.6.0/19.6.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nx/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nx/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nx/19.6.0/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nx/19.6.0/19.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| openresty/openresty | `1.25.3.1-0-buster` -> `1.25.3.2-0-buster` | [![age](https://developer.mend.io/api/mc/badges/age/docker/openresty%2fopenresty/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/docker/openresty%2fopenresty/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/docker/openresty%2fopenresty/1.25.3.1/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/docker/openresty%2fopenresty/1.25.3.1/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | final | patch |
| [playwright](https://playwright.dev) ([source](https://togithub.com/microsoft/playwright)) | [`=1.44.1` -> `=1.46.1`](https://renovatebot.com/diffs/npm/playwright/1.44.1/1.46.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/playwright/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/playwright/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/playwright/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/playwright/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [react-refresh](https://reactjs.org/) ([source](https://togithub.com/facebook/react/tree/HEAD/packages/react)) | [`^0.10.0` -> `^0.14.0`](https://renovatebot.com/diffs/npm/react-refresh/0.10.0/0.14.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-refresh/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-refresh/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-refresh/0.10.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-refresh/0.10.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [serde](https://serde.rs) ([source](https://togithub.com/serde-rs/serde)) | `1.0.204` -> `1.0.208` | [![age](https://developer.mend.io/api/mc/badges/age/crate/serde/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/serde/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/serde/1.0.204/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/serde/1.0.204/1.0.208?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [serde_json](https://togithub.com/serde-rs/json) | `1.0.120` -> `1.0.125` | [![age](https://developer.mend.io/api/mc/badges/age/crate/serde_json/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/serde_json/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/serde_json/1.0.120/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/serde_json/1.0.120/1.0.125?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [storybook-dark-mode](https://togithub.com/hipstersmoothie/storybook-dark-mode) | [`4.0.1` -> `4.0.2`](https://renovatebot.com/diffs/npm/storybook-dark-mode/4.0.1/4.0.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/storybook-dark-mode/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/storybook-dark-mode/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/storybook-dark-mode/4.0.1/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/storybook-dark-mode/4.0.1/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [tokio](https://tokio.rs) ([source](https://togithub.com/tokio-rs/tokio)) | `1.38.0` -> `1.39.3` | [![age](https://developer.mend.io/api/mc/badges/age/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dev-dependencies | minor |
| [tokio](https://tokio.rs) ([source](https://togithub.com/tokio-rs/tokio)) | `1.38.0` -> `1.39.3` | [![age](https://developer.mend.io/api/mc/badges/age/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/tokio/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/tokio/1.38.0/1.39.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | minor |
| [undici](https://undici.nodejs.org) ([source](https://togithub.com/nodejs/undici)) | [`6.19.7` -> `6.19.8`](https://renovatebot.com/diffs/npm/undici/6.19.7/6.19.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/undici/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/undici/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/undici/6.19.7/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/undici/6.19.7/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [undici](https://undici.nodejs.org) ([source](https://togithub.com/nodejs/undici)) | [`6.19.7` -> `6.19.8`](https://renovatebot.com/diffs/npm/undici/6.19.7/6.19.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/undici/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/undici/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/undici/6.19.7/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/undici/6.19.7/6.19.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [vite-plugin-dts](https://togithub.com/qmhc/vite-plugin-dts) | [`4.0.2` -> `4.0.3`](https://renovatebot.com/diffs/npm/vite-plugin-dts/4.0.2/4.0.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/vite-plugin-dts/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite-plugin-dts/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite-plugin-dts/4.0.2/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite-plugin-dts/4.0.2/4.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |

---

### Release Notes

<details>
<summary>fal-ai/fal-js (@&#8203;fal-ai/serverless-client)</summary>

### [`v0.14.2`](c3a3c3d21a...b3ab5f0e15)

[Compare Source](c3a3c3d21a...b3ab5f0e15)

### [`v0.14.1`](6edbf2948d...c3a3c3d21a)

[Compare Source](6edbf2948d...c3a3c3d21a)

### [`v0.14.0`](cf300e9cc0...6edbf2948d)

[Compare Source](cf300e9cc0...6edbf2948d)

</details>

<details>
<summary>napi-rs/napi-rs (@&#8203;napi-rs/cli)</summary>

### [`v3.0.0-alpha.62`](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.61...@napi-rs/cli@3.0.0-alpha.62)

[Compare Source](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.61...@napi-rs/cli@3.0.0-alpha.62)

### [`v3.0.0-alpha.61`](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.60...@napi-rs/cli@3.0.0-alpha.61)

[Compare Source](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.60...@napi-rs/cli@3.0.0-alpha.61)

</details>

<details>
<summary>nrwl/nx (@&#8203;nx/vite)</summary>

### [`v19.6.1`](https://togithub.com/nrwl/nx/releases/tag/19.6.1)

[Compare Source](https://togithub.com/nrwl/nx/compare/19.6.0...19.6.1)

##### 19.6.1 (2024-08-19)

##### 🚀 Features

-   **core:** add shutdown lifecycle hook to node executor ([#&#8203;27354](https://togithub.com/nrwl/nx/pull/27354))
-   **docs:** update OpenAI model to use gpt-4o-mini ([#&#8203;27434](https://togithub.com/nrwl/nx/pull/27434))
-   **nx-dev:** add Explain with AI to Enterprise and Pro Plans ([#&#8203;27455](https://togithub.com/nrwl/nx/pull/27455))
-   **nx-dev:** add Monorepo World banner to hero ([#&#8203;27482](https://togithub.com/nrwl/nx/pull/27482))

##### 🩹 Fixes

-   **angular:** generate [@&#8203;nx/angular](https://togithub.com/nx/angular) in devDependencies and move to dependencies when using runtime helpers ([#&#8203;27405](https://togithub.com/nrwl/nx/pull/27405))
-   **angular:** module-federation-ssr-dev-server should call correct builder ([#&#8203;27477](https://togithub.com/nrwl/nx/pull/27477))
-   **angular:** bump ngrx version to 18.0.2 ([#&#8203;27506](https://togithub.com/nrwl/nx/pull/27506))
-   **js:** handle arbitrary nested ts path mappings when re-mapping them to the outputs ([#&#8203;27429](https://togithub.com/nrwl/nx/pull/27429))
-   **linter:** update the [@&#8203;nx/dependency-checks](https://togithub.com/nx/dependency-checks) rule to read the package.json content from the rule context ([#&#8203;27476](https://togithub.com/nrwl/nx/pull/27476))
-   **misc:** ensure custom reporters are usable with [@&#8203;nx/playwright](https://togithub.com/nx/playwright):playwright ([#&#8203;27443](https://togithub.com/nrwl/nx/pull/27443))
-   **module-federation:** ensure target defaults are set correctly [#&#8203;27448](https://togithub.com/nrwl/nx/issues/27448) ([#&#8203;27472](https://togithub.com/nrwl/nx/pull/27472), [#&#8203;27448](https://togithub.com/nrwl/nx/issues/27448))
-   **node:** build-esbuild-options.ts browser user define envs by config ([#&#8203;27480](https://togithub.com/nrwl/nx/pull/27480))
-   **nx-dev:** Og image path generation ([#&#8203;27456](https://togithub.com/nrwl/nx/pull/27456))
-   **nx-dev:** bad link from home page ([#&#8203;27475](https://togithub.com/nrwl/nx/pull/27475))
-   **nx-dev:** fix use `key` instead of `env` to reference cache key in… ([#&#8203;26644](https://togithub.com/nrwl/nx/pull/26644))
-   **react:** ensure [@&#8203;vitejs/plugin-react](https://togithub.com/vitejs/plugin-react) is installed for storybook ([#&#8203;27463](https://togithub.com/nrwl/nx/pull/27463))
-   **react:** enable vitejs-plugin-react-swc for swc compiler ([#&#8203;27457](https://togithub.com/nrwl/nx/pull/27457))
-   **repo:** add dependsOn to native build ([#&#8203;27446](https://togithub.com/nrwl/nx/pull/27446))
-   **testing:** fix misc issues in migrations ([#&#8203;27471](https://togithub.com/nrwl/nx/pull/27471))

##### ❤️  Thank You

-   [@&#8203;NgDaddy](https://togithub.com/NgDaddy) [@&#8203;NgDaddy](https://togithub.com/NgDaddy)
-   Benjamin Cabanes [@&#8203;bcabanes](https://togithub.com/bcabanes)
-   Colum Ferry [@&#8203;Coly010](https://togithub.com/Coly010)
-   EGonz1PCTY
-   Isaac Mann [@&#8203;isaacplmann](https://togithub.com/isaacplmann)
-   Jason Jean [@&#8203;FrozenPandaz](https://togithub.com/FrozenPandaz)
-   Jonathan Cammisuli
-   Kamenskih Dmitriy
-   Leosvel Pérez Espinosa [@&#8203;leosvelperez](https://togithub.com/leosvelperez)
-   Nicholas Cunningham [@&#8203;ndcunningham](https://togithub.com/ndcunningham)
-   Tine Kondo [@&#8203;tinesoft](https://togithub.com/tinesoft)
-   Wei Liang [@&#8203;weiliang79](https://togithub.com/weiliang79)

</details>

<details>
<summary>microsoft/playwright (@&#8203;playwright/test)</summary>

### [`v1.46.1`](https://togithub.com/microsoft/playwright/compare/v1.46.0...e1c861cfa7a6caf3c5b798786b1e6298c4f3cf31)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.46.0...v1.46.1)

### [`v1.46.0`](https://togithub.com/microsoft/playwright/compare/v1.45.3...99a36310570617222290c09b96a2026beb8b00f9)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.3...v1.46.0)

### [`v1.45.3`](https://togithub.com/microsoft/playwright/compare/v1.45.2...0e130fa8edaf85765c4a5a86bded0e6d33bfd7c2)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.2...v1.45.3)

### [`v1.45.2`](https://togithub.com/microsoft/playwright/compare/v1.45.1...d8a5f3b33193e413b404ff4aa1f71e859d8f1b6b)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.1...v1.45.2)

### [`v1.45.1`](https://togithub.com/microsoft/playwright/compare/v1.45.0...e8989f83d9801cdaadc3803b5341c601c9593947)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.45.0...v1.45.1)

### [`v1.45.0`](https://togithub.com/microsoft/playwright/compare/v1.44.1...4f3f6eecae490af444dd9298c9eaeb0c596915b7)

[Compare Source](https://togithub.com/microsoft/playwright/compare/v1.44.1...v1.45.0)

</details>

<details>
<summary>swc-project/swc (@&#8203;swc/core)</summary>

### [`v1.7.14`](https://togithub.com/swc-project/swc/blob/HEAD/CHANGELOG.md#1714---2024-08-19)

[Compare Source](https://togithub.com/swc-project/swc/compare/v1.7.12...v1.7.14)

##### Bug Fixes

-   **(common)** Use `SourceMap::adjust_mappings` in correct order ([#&#8203;9447](https://togithub.com/swc-project/swc/issues/9447)) ([05961eb](05961eb018))

-   **(es)** Preserve more comments ([#&#8203;9449](https://togithub.com/swc-project/swc/issues/9449)) ([673655c](673655c169))

##### Features

-   **(es/decorators)** Groundwork for stage 3 decorator ([#&#8203;9450](https://togithub.com/swc-project/swc/issues/9450)) ([238ba8b](238ba8b1d2))

##### Refactor

-   **(visit)** Remove `VisitAll` ([#&#8203;9448](https://togithub.com/swc-project/swc/issues/9448)) ([8845b76](8845b76ac4))

</details>

<details>
<summary>i18next/i18next (i18next)</summary>

### [`v23.14.0`](https://togithub.com/i18next/i18next/blob/HEAD/CHANGELOG.md#23140)

[Compare Source](https://togithub.com/i18next/i18next/compare/v23.13.0...v23.14.0)

-   If backend errors with retry flag, set internal state to 0, so reloadingResources should work [147](https://togithub.com/i18next/i18next-http-backend/issues/147)

</details>

<details>
<summary>lucide-icons/lucide (lucide-react)</summary>

### [`v0.428.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.428.0): New icons 0.428.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.427.0...0.428.0)

#### New icons 🎨

-   `tickets-plane` ([#&#8203;2196](https://togithub.com/lucide-icons/lucide/issues/2196)) by [@&#8203;jguddas](https://togithub.com/jguddas)

#### Modified Icons 🔨

-   `folder-search` ([#&#8203;2354](https://togithub.com/lucide-icons/lucide/issues/2354)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.427.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.427.0): New icons 0.427.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.426.0...0.427.0)

#### New icons 🎨

-   `binoculars` ([#&#8203;2207](https://togithub.com/lucide-icons/lucide/issues/2207)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `tickets` ([#&#8203;2335](https://togithub.com/lucide-icons/lucide/issues/2335)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.426.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.426.0): New icons 0.426.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.425.0...0.426.0)

#### New icons 🎨

-   `chevrons-left-right-ellipsis` ([#&#8203;2120](https://togithub.com/lucide-icons/lucide/issues/2120)) by [@&#8203;ericfennis](https://togithub.com/ericfennis)
-   `ethernet-port` ([#&#8203;2120](https://togithub.com/lucide-icons/lucide/issues/2120)) by [@&#8203;ericfennis](https://togithub.com/ericfennis)

#### Modified Icons 🔨

-   `cigarette-off` ([#&#8203;2282](https://togithub.com/lucide-icons/lucide/issues/2282)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `cigarette` ([#&#8203;2282](https://togithub.com/lucide-icons/lucide/issues/2282)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.425.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.425.0): New icons 0.425.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.424.0...0.425.0)

#### New icons 🎨

-   `bandage` ([#&#8203;2341](https://togithub.com/lucide-icons/lucide/issues/2341)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `table-of-contents` ([#&#8203;2348](https://togithub.com/lucide-icons/lucide/issues/2348)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `mouse-pointer-2` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `mouse-pointer-ban` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `mouse-pointer-click` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `mouse-pointer` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `square-dashed-mouse-pointer` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `square-mouse-pointer` ([#&#8203;2350](https://togithub.com/lucide-icons/lucide/issues/2350)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.424.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.424.0): New icons 0.424.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.423.0...0.424.0)

#### New icons 🎨

-   `map-pin-house` ([#&#8203;2337](https://togithub.com/lucide-icons/lucide/issues/2337)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `replace-all` ([#&#8203;2333](https://togithub.com/lucide-icons/lucide/issues/2333)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `replace` ([#&#8203;2333](https://togithub.com/lucide-icons/lucide/issues/2333)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.423.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.423.0): New icons 0.423.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.422.0...0.423.0)

#### New icons 🎨

-   `amphora` ([#&#8203;1926](https://togithub.com/lucide-icons/lucide/issues/1926)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.422.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.422.0): New icons 0.422.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.421.0...0.422.0)

#### Modified Icons 🔨

-   `skull` ([#&#8203;2197](https://togithub.com/lucide-icons/lucide/issues/2197)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.421.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.421.0): New icons 0.421.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.420.0...0.421.0)

#### New icons 🎨

-   `microchip` ([#&#8203;1982](https://togithub.com/lucide-icons/lucide/issues/1982)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `circle-check-big` ([#&#8203;2330](https://togithub.com/lucide-icons/lucide/issues/2330)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `cloud-download` ([#&#8203;2355](https://togithub.com/lucide-icons/lucide/issues/2355)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `pentagon` ([#&#8203;1918](https://togithub.com/lucide-icons/lucide/issues/1918)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `square-check-big` ([#&#8203;2331](https://togithub.com/lucide-icons/lucide/issues/2331)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.420.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.420.0): New icons 0.420.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.419.0...0.420.0)

#### New icons 🎨

-   `omega` ([#&#8203;2347](https://togithub.com/lucide-icons/lucide/issues/2347)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `calendar-search` ([#&#8203;2351](https://togithub.com/lucide-icons/lucide/issues/2351)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `cloud-upload` ([#&#8203;2352](https://togithub.com/lucide-icons/lucide/issues/2352)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.419.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.419.0): New icons 0.419.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.418.0...0.419.0)

#### New icons 🎨

-   `circle-fading-arrow-up` ([#&#8203;2287](https://togithub.com/lucide-icons/lucide/issues/2287)) by [@&#8203;mosch](https://togithub.com/mosch)

### [`v0.418.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.418.0): New icons 0.418.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.417.0...0.418.0)

#### New icons 🎨

-   `id-card` ([#&#8203;1296](https://togithub.com/lucide-icons/lucide/issues/1296)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.417.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.417.0): New icons 0.417.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.416.0...0.417.0)

#### Modified Icons 🔨

-   `chart-column-increasing` ([#&#8203;2334](https://togithub.com/lucide-icons/lucide/issues/2334)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.416.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.416.0): New icons 0.416.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.415.0...0.416.0)

#### New icons 🎨

-   `map-pin-check-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-check` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-minus-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-minus` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-plus-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-plus` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-x-inside` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin-x` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

#### Modified Icons 🔨

-   `map-pin-off` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pin` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `map-pinned` ([#&#8203;2301](https://togithub.com/lucide-icons/lucide/issues/2301)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.415.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.415.0): New icons 0.415.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.414.0...0.415.0)

#### New icons 🎨

-   `square-square` ([#&#8203;2241](https://togithub.com/lucide-icons/lucide/issues/2241)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.414.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.414.0): New icons 0.414.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.413.0...0.414.0)

#### New icons 🎨

-   `chart-area` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-bar-decreasing` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-bar-increasing` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-bar-stacked` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-column-big` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-column-increasing` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-column-stacked` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-network` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-no-axes-combined` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `chart-spline` ([#&#8203;2219](https://togithub.com/lucide-icons/lucide/issues/2219)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

### [`v0.413.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.413.0): New icons 0.413.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.412.0...0.413.0)

#### New icons 🎨

-   `dam` ([#&#8203;2233](https://togithub.com/lucide-icons/lucide/issues/2233)) by [@&#8203;AndreasSas](https://togithub.com/AndreasSas)

#### Modified Icons 🔨

-   `dog` ([#&#8203;2249](https://togithub.com/lucide-icons/lucide/issues/2249)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `key-square` ([#&#8203;2277](https://togithub.com/lucide-icons/lucide/issues/2277)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.412.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.412.0): New icons 0.412.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.411.0...0.412.0)

#### New icons 🎨

-   `letter-text` ([#&#8203;2252](https://togithub.com/lucide-icons/lucide/issues/2252)) by [@&#8203;GRA0007](https://togithub.com/GRA0007)

### [`v0.411.0`](https://togithub.com/lucide-icons/lucide/compare/0.410.0...0.411.0)

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.410.0...0.411.0)

### [`v0.410.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.410.0): New icons 0.410.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.409.0...0.410.0)

#### New icons 🎨

-   `philippine-peso` ([#&#8203;2231](https://togithub.com/lucide-icons/lucide/issues/2231)) by [@&#8203;kasutu](https://togithub.com/kasutu)

#### Modified Icons 🔨

-   `ribbon` ([#&#8203;2271](https://togithub.com/lucide-icons/lucide/issues/2271)) by [@&#8203;jguddas](https://togithub.com/jguddas)

### [`v0.409.0`](https://togithub.com/lucide-icons/lucide/releases/tag/0.409.0): New icons 0.409.0

[Compare Source](https://togithub.com/lucide-icons/lucide/compare/0.408.0...0.409.0)

#### Modified Icons 🔨

-   `calendar-minus` ([#&#8203;2265](https://togithub.com/lucide-icons/lucide/issues/2265)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `eye-off` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `eye` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `image-plus` ([#&#8203;2321](https://togithub.com/lucide-icons/lucide/issues/2321)) by [@&#8203;jguddas](https://togithub.com/jguddas)
-   `scan-eye` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)
-   `view` ([#&#8203;2317](https://togithub.com/lucide-icons/lucide/issues/2317)) by [@&#8203;karsa-mistmere](https://togithub.com/karsa-mistmere)

</details>

<details>
<summary>webpack-contrib/mini-css-extract-plugin (mini-css-extract-plugin)</summary>

### [`v2.9.1`](https://togithub.com/webpack-contrib/mini-css-extract-plugin/blob/HEAD/CHANGELOG.md#291-2024-08-19)

[Compare Source](https://togithub.com/webpack-contrib/mini-css-extract-plugin/compare/v2.9.0...v2.9.1)

</details>

<details>
<summary>nodejs/node (node)</summary>

### [`v20.16.0`](https://togithub.com/nodejs/node/compare/v20.15.1...v20.16.0)

[Compare Source](https://togithub.com/nodejs/node/compare/v20.15.1...v20.16.0)

</details>

<details>
<summary>facebook/react (react-refresh)</summary>

### [`v0.14.2`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#0142-November-2-2015)

[Compare Source](https://togithub.com/facebook/react/compare/v0.14.1...v0.14.2)

##### React DOM

-   Fixed bug with development build preventing events from firing in some versions of Internet Explorer & Edge
-   Fixed bug with development build when using es5-sham in older versions of Internet Explorer
-   Added support for `integrity` attribute
-   Fixed bug resulting in `children` prop being coerced to a string for custom elements, which was not the desired behavior
-   Moved `react` from `dependencies` to `peerDependencies` to match expectations and align with `react-addons-*` packages

### [`v0.14.1`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#01410-October-14-2020)

[Compare Source](https://togithub.com/facebook/react/compare/v0.14.0...v0.14.1)

##### React

-   Backport support for the [new JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) to 0.14.x. ([@&#8203;lunaruan](https://togithub.com/lunaruan) in [#&#8203;18299](https://togithub.com/facebook/react/pull/18299) and [@&#8203;gaearon](https://togithub.com/gaearon) in [#&#8203;20024](https://togithub.com/facebook/react/pull/20024))

### [`v0.14.0`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#0140-October-7-2015)

[Compare Source](https://togithub.com/facebook/react/compare/v0.13.0...v0.14.0)

##### Major changes

-   Split the main `react` package into two: `react` and `react-dom`.  This paves the way to writing components that can be shared between the web version of React and React Native.  This means you will need to include both files and some functions have been moved from `React` to `ReactDOM`.
-   Addons have been moved to separate packages (`react-addons-clone-with-props`, `react-addons-create-fragment`, `react-addons-css-transition-group`, `react-addons-linked-state-mixin`, `react-addons-perf`, `react-addons-pure-render-mixin`, `react-addons-shallow-compare`, `react-addons-test-utils`, `react-addons-transition-group`, `react-addons-update`, `ReactDOM.unstable_batchedUpdates`).
-   Stateless functional components - React components were previously created using React.createClass or using ES6 classes.  This release adds a [new syntax](https://reactjs.org/docs/reusable-components.html#stateless-functions) where a user defines a single [stateless render function](https://reactjs.org/docs/reusable-components.html#stateless-functions) (with one parameter: `props`) which returns a JSX element, and this function may be used as a component.
-   Refs to DOM components as the DOM node itself. Previously the only useful thing you can do with a DOM component is call `getDOMNode()` to get the underlying DOM node. Starting with this release, a ref to a DOM component *is* the actual DOM node. **Note that refs to custom (user-defined) components work exactly as before; only the built-in DOM components are affected by this change.**

##### Breaking changes

-   `React.initializeTouchEvents` is no longer necessary and has been removed completely. Touch events now work automatically.
-   Add-Ons: Due to the DOM node refs change mentioned above, `TestUtils.findAllInRenderedTree` and related helpers are no longer able to take a DOM component, only a custom component.
-   The `props` object is now frozen, so mutating props after creating a component element is no longer supported. In most cases, [`React.cloneElement`](https://reactjs.org/docs/react-api.html#cloneelement) should be used instead. This change makes your components easier to reason about and enables the compiler optimizations mentioned above.
-   Plain objects are no longer supported as React children; arrays should be used instead. You can use the [`createFragment`](https://reactjs.org/docs/create-fragment.html) helper to migrate, which now returns an array.
-   Add-Ons: `classSet` has been removed. Use [classnames](https://togithub.com/JedWatson/classnames) instead.
-   Web components (custom elements) now use native property names.  Eg: `class` instead of `className`.

##### Deprecations

-   `this.getDOMNode()` is now deprecated and `ReactDOM.findDOMNode(this)` can be used instead. Note that in the common case, `findDOMNode` is now unnecessary since a ref to the DOM component is now the actual DOM node.
-   `setProps` and `replaceProps` are now deprecated. Instead, call ReactDOM.render again at the top level with the new props.
-   ES6 component classes must now extend `React.Component` in order to enable stateless function components. The [ES3 module pattern](https://reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html#other-languages) will continue to work.
-   Reusing and mutating a `style` object between renders has been deprecated. This mirrors our change to freeze the `props` object.
-   Add-Ons: `cloneWithProps` is now deprecated. Use [`React.cloneElement`](https://reactjs.org/docs/react-api.html#cloneelement) instead (unlike `cloneWithProps`, `cloneElement` does not merge `className` or `style` automatically; you can merge them manually if needed).
-   Add-Ons: To improve reliability, `CSSTransitionGroup` will no longer listen to transition events. Instead, you should specify transition durations manually using props such as `transitionEnterTimeout={500}`.

##### Notable enhancements

-   Added `React.Children.toArray` which takes a nested children object and returns a flat array with keys assigned to each child. This helper makes it easier to manipulate collections of children in your `render` methods, especially if you want to reorder or slice `this.props.children` before passing it down. In addition, `React.Children.map` now returns plain arrays too.
-   React uses `console.error` instead of `console.warn` for warnings so that browsers show a full stack trace in the console. (Our warnings appear when you use patterns that will break in future releases and for code that is likely to behave unexpectedly, so we do consider our warnings to be “must-fix” errors.)
-   Previously, including untrusted objects as React children [could result in an XSS security vulnerability](http://danlec.com/blog/xss-via-a-spoofed-react-element). This problem should be avoided by properly validating input at the application layer and by never passing untrusted objects around your application code. As an additional layer of protection, [React now tags elements](https://togithub.com/facebook/react/pull/4832) with a specific [ES2015 (ES6) `Symbol`](http://www.2ality.com/2014/12/es6-symbols.html) in browsers that support it, in order to ensure that React never considers untrusted JSON to be a valid element. If this extra security protection is important to you, you should add a `Symbol` polyfill for older browsers, such as the one included by [Babel’s polyfill](https://babeljs.io/docs/usage/polyfill/).
-   When possible, React DOM now generates XHTML-compatible markup.
-   React DOM now supports these standard HTML attributes: `capture`, `challenge`, `inputMode`, `is`, `keyParams`, `keyType`, `minLength`, `summary`, `wrap`. It also now supports these non-standard attributes: `autoSave`, `results`, `security`.
-   React DOM now supports these SVG attributes, which render into namespaced attributes: `xlinkActuate`, `xlinkArcrole`, `xlinkHref`, `xlinkRole`, `xlinkShow`, `xlinkTitle`, `xlinkType`, `xmlBase`, `xmlLang`, `xmlSpace`.
-   The `image` SVG tag is now supported by React DOM.
-   In React DOM, arbitrary attributes are supported on custom elements (those with a hyphen in the tag name or an `is="..."` attribute).
-   React DOM now supports these media events on `audio` and `video` tags: `onAbort`, `onCanPlay`, `onCanPlayThrough`, `onDurationChange`, `onEmptied`, `onEncrypted`, `onEnded`, `onError`, `onLoadedData`, `onLoadedMetadata`, `onLoadStart`, `onPause`, `onPlay`, `onPlaying`, `onProgress`, `onRateChange`, `onSeeked`, `onSeeking`, `onStalled`, `onSuspend`, `onTimeUpdate`, `onVolumeChange`, `onWaiting`.
-   Many small performance improvements have been made.
-   Many warnings show more context than before.
-   Add-Ons: A [`shallowCompare`](https://togithub.com/facebook/react/pull/3355) add-on has been added as a migration path for `PureRenderMixin` in ES6 classes.
-   Add-Ons: `CSSTransitionGroup` can now use [custom class names](https://togithub.com/facebook/react/blob/48942b85/docs/docs/10.1-animation.md#custom-classes) instead of appending `-enter-active` or similar to the transition name.

##### New helpful warnings

-   React DOM now warns you when nesting HTML elements invalidly, which helps you avoid surprising errors during updates.
-   Passing `document.body` directly as the container to `ReactDOM.render` now gives a warning as doing so can cause problems with browser extensions that modify the DOM.
-   Using multiple instances of React together is not supported, so we now warn when we detect this case to help you avoid running into the resulting problems.

##### Notable bug fixes

-   Click events are handled by React DOM more reliably in mobile browsers, particularly in Mobile Safari.
-   SVG elements are created with the correct namespace in more cases.
-   React DOM now renders `<option>` elements with multiple text children properly and renders `<select>` elements on the server with the correct option selected.
-   When two separate copies of React add nodes to the same document (including when a browser extension uses React), React DOM tries harder not to throw exceptions during event handling.
-   Using non-lowercase HTML tag names in React DOM (e.g., `React.createElement('DIV')`) no longer causes problems, though we continue to recommend lowercase for consistency with the JSX tag name convention (lowercase names refer to built-in components, capitalized names refer to custom components).
-   React DOM understands that these CSS properties are unitless and does not append “px” to their values: `animationIterationCount`, `boxOrdinalGroup`, `flexOrder`, `tabSize`, `stopOpacity`.
-   Add-Ons: When using the test utils, `Simulate.mouseEnter` and `Simulate.mouseLeave` now work.
-   Add-Ons: ReactTransitionGroup now correctly handles multiple nodes being removed simultaneously.

##### React Tools / Babel

##### Breaking Changes

-   The `react-tools` package and `JSXTransformer.js` browser file [have been deprecated](https://reactjs.org/blog/2015/06/12/deprecating-jstransform-and-react-tools.html). You can continue using version `0.13.3` of both, but we no longer support them and recommend migrating to [Babel](https://babeljs.io), which has built-in support for React and JSX.

##### New Features

-   Babel 5.8.24 introduces **Inlining React elements:** The `optimisation.react.inlineElements` transform converts JSX elements to object literals like `{type: 'div', props: ...}` instead of calls to `React.createElement`.  This should only be enabled in production, since it disables some development warnings/checks.
-   Babel 5.8.24 introduces **Constant hoisting for React elements:** The `optimisation.react.constantElements` transform hoists element creation to the top level for subtrees that are fully static, which reduces calls to `React.createElement` and the resulting allocations. More importantly, it tells React that the subtree hasn’t changed so React can completely skip it when reconciling.  This should only be enabled in production, since it disables some development warnings/checks.

### [`v0.13.0`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#0130-March-10-2015)

[Compare Source](https://togithub.com/facebook/react/compare/v0.12.0...v0.13.0)

##### React Core

##### Breaking Changes

-   Deprecated patterns that warned in 0.12 no longer work: most prominently, calling component classes without using JSX or React.createElement and using non-component functions with JSX or createElement
-   Mutating `props` after an element is created is deprecated and will cause warnings in development mode; future versions of React will incorporate performance optimizations assuming that props aren't mutated
-   Static methods (defined in `statics`) are no longer autobound to the component class
-   `ref` resolution order has changed slightly such that a ref to a component is available immediately after its `componentDidMount` method is called; this change should be observable only if your component calls a parent component's callback within your `componentDidMount`, which is an anti-pattern and should be avoided regardless
-   Calls to `setState` in life-cycle methods are now always batched and therefore asynchronous. Previously the first call on the first mount was synchronous.
-   `setState` and `forceUpdate` on an unmounted component now warns instead of throwing. That avoids a possible race condition with Promises.
-   Access to most internal properties has been completely removed, including `this._pendingState` and `this._rootNodeID`.

##### New Features

-   Support for using ES6 classes to build React components; see the [v0.13.0 beta 1 notes](https://reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html) for details.
-   Added new top-level API `React.findDOMNode(component)`, which should be used in place of `component.getDOMNode()`. The base class for ES6-based components will not have `getDOMNode`. This change will enable some more patterns moving forward.
-   Added a new top-level API `React.cloneElement(el, props)` for making copies of React elements – see the [v0.13 RC2 notes](https://reactjs.org/blog/2015/03/03/react-v0.13-rc2.html#react.cloneelement) for more details.
-   New `ref` style, allowing a callback to be used in place of a name: `<Photo ref={(c) => this._photo = c} />` allows you to reference the component with `this._photo` (as opposed to `ref="photo"` which gives `this.refs.photo`).
-   `this.setState()` can now take a function as the first argument for transactional state updates, such as `this.setState((state, props) => ({count: state.count + 1}));` – this means that you no longer need to use `this._pendingState`, which is now gone.
-   Support for iterators and immutable-js sequences as children.

##### Deprecations

-   `ComponentClass.type` is deprecated. Just use `ComponentClass` (usually as `element.type === ComponentClass`).
-   Some methods that are available on `createClass`-based components are removed or deprecated from ES6 classes (`getDOMNode`, `replaceState`, `isMounted`, `setProps`, `replaceProps`).

##### React with Add-Ons

##### New Features

-   [`React.addons.createFragment` was added](https://reactjs.org/docs/create-fragment.html) for adding keys to entire sets of children.

##### Deprecations

-   `React.addons.classSet` is now deprecated. This functionality can be replaced with several freely available modules. [classnames](https://www.npmjs.com/package/classnames) is one such module.
-   Calls to `React.addons.cloneWithProps` can be migrated to use `React.cloneElement` instead – make sure to merge `style` and `className` manually if desired.

##### React Tools

##### Breaking Changes

-   When transforming ES6 syntax, `class` methods are no longer enumerable by default, which requires `Object.defineProperty`; if you support browsers such as IE8, you can pass `--target es3` to mirror the old behavior

##### New Features

-   `--target` option is available on the jsx command, allowing users to specify and ECMAScript version to target.
    -   `es5` is the default.
    -   `es3` restores the previous default behavior. An additional transform is added here to ensure the use of reserved words as properties is safe (eg `this.static` will become `this['static']` for IE8 compatibility).
-   The transform for the call spread operator has also been enabled.

##### JSXTransformer

##### Breaking Changes

-   The return value of `transform` now contains `sourceMap` as a JS object already, not an instance of `SourceMapGenerator`.

##### JSX

##### Breaking Changes

-   A change was made to how some JSX was parsed, specifically around the use of `>` or `}` when inside an element. Previously it would be treated as a string but now it will be treated as a parse error. The [`jsx_orphaned_brackets_transformer`](https://www.npmjs.com/package/jsx_orphaned_brackets_transformer) package on npm can be used to find and fix potential issues in your JSX code.

### [`v0.12.0`](https://togithub.com/facebook/react/blob/HEAD/CHANGELOG.md#0120-October-28-2014)

[Compare Source](https://togithub.com/facebook/react/compare/v0.11.0...v0.12.0)

##### React Core

##### Breaking Changes

-   `key` and `ref` moved off props object, now

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-19 18:34:45 +00:00
pengx17
9d42db56ca chore: bump electron version (#7922) 2024-08-19 14:24:34 +00:00
pengx17
f3930a9262 chore(core): remove @storybook/test dep (#7923)
we no longer need  "@storybook/test" and it indirectly has an incompatible prettier version 3.3.2 which differs from what we are using "3.3.3".

removing it to resolve this issue
2024-08-19 13:50:06 +00:00
renovate
10bea3922e chore: bump up all non-major dependencies (#7615)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence | Type | Update |
|---|---|---|---|---|---|---|---|
| [@apollo/server](https://togithub.com/apollographql/apollo-server) ([source](https://togithub.com/apollographql/apollo-server/tree/HEAD/packages/server)) | [`4.10.5` -> `4.11.0`](https://renovatebot.com/diffs/npm/@apollo%2fserver/4.10.5/4.11.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@apollo%2fserver/4.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@apollo%2fserver/4.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@apollo%2fserver/4.10.5/4.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@apollo%2fserver/4.10.5/4.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@atlaskit/pragmatic-drag-and-drop](https://atlassian.design/components/pragmatic-drag-and-drop/) ([source](https://togithub.com/atlassian/pragmatic-drag-and-drop)) | [`1.2.1` -> `1.3.0`](https://renovatebot.com/diffs/npm/@atlaskit%2fpragmatic-drag-and-drop/1.2.1/1.3.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@atlaskit%2fpragmatic-drag-and-drop/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@atlaskit%2fpragmatic-drag-and-drop/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@atlaskit%2fpragmatic-drag-and-drop/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@atlaskit%2fpragmatic-drag-and-drop/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@aws-sdk/client-s3](https://togithub.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) ([source](https://togithub.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)) | [`3.620.0` -> `3.633.0`](https://renovatebot.com/diffs/npm/@aws-sdk%2fclient-s3/3.620.0/3.633.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@aws-sdk%2fclient-s3/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@aws-sdk%2fclient-s3/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@aws-sdk%2fclient-s3/3.620.0/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@aws-sdk%2fclient-s3/3.620.0/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@aws-sdk/client-s3](https://togithub.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) ([source](https://togithub.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)) | [`3.620.0` -> `3.633.0`](https://renovatebot.com/diffs/npm/@aws-sdk%2fclient-s3/3.620.0/3.633.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@aws-sdk%2fclient-s3/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@aws-sdk%2fclient-s3/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@aws-sdk%2fclient-s3/3.620.0/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@aws-sdk%2fclient-s3/3.620.0/3.633.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@commitlint/cli](https://commitlint.js.org/) ([source](https://togithub.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli)) | [`19.3.0` -> `19.4.0`](https://renovatebot.com/diffs/npm/@commitlint%2fcli/19.3.0/19.4.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@commitlint%2fcli/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@commitlint%2fcli/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@commitlint%2fcli/19.3.0/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@commitlint%2fcli/19.3.0/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@fal-ai/serverless-client](https://togithub.com/fal-ai/fal-js) ([source](https://togithub.com/fal-ai/fal-js/tree/HEAD/libs/client)) | [`^0.13.0` -> `^0.14.0`](https://renovatebot.com/diffs/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@fal-ai%2fserverless-client/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@fal-ai%2fserverless-client/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@fal-ai%2fserverless-client/0.13.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@floating-ui/dom](https://floating-ui.com) ([source](https://togithub.com/floating-ui/floating-ui/tree/HEAD/packages/dom)) | [`1.6.8` -> `1.6.10`](https://renovatebot.com/diffs/npm/@floating-ui%2fdom/1.6.8/1.6.10) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@floating-ui%2fdom/1.6.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@floating-ui%2fdom/1.6.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@floating-ui%2fdom/1.6.8/1.6.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@floating-ui%2fdom/1.6.8/1.6.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [@napi-rs/cli](https://togithub.com/napi-rs/napi-rs) | [`3.0.0-alpha.60` -> `3.0.0-alpha.62`](https://renovatebot.com/diffs/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@napi-rs%2fcli/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@napi-rs%2fcli/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@napi-rs%2fcli/3.0.0-alpha.60/3.0.0-alpha.62?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@nestjs/common](https://nestjs.com) ([source](https://togithub.com/nestjs/nest/tree/HEAD/packages/common)) | [`10.3.10` -> `10.4.1`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/10.3.10/10.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcommon/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcommon/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@nestjs/core](https://nestjs.com) ([source](https://togithub.com/nestjs/nest/tree/HEAD/packages/core)) | [`10.3.10` -> `10.4.1`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/10.3.10/10.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcore/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcore/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@nestjs/platform-express](https://nestjs.com) ([source](https://togithub.com/nestjs/nest/tree/HEAD/packages/platform-express)) | [`10.3.10` -> `10.4.1`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/10.3.10/10.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-express/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-express/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-express/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-express/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@nestjs/platform-socket.io](https://nestjs.com) ([source](https://togithub.com/nestjs/nest/tree/HEAD/packages/platform-socket.io)) | [`10.3.10` -> `10.4.1`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-socket.io/10.3.10/10.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-socket.io/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-socket.io/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-socket.io/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-socket.io/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@nestjs/testing](https://nestjs.com) ([source](https://togithub.com/nestjs/nest/tree/HEAD/packages/testing)) | [`10.3.10` -> `10.4.1`](https://renovatebot.com/diffs/npm/@nestjs%2ftesting/10.3.10/10.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2ftesting/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2ftesting/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2ftesting/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2ftesting/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@nestjs/websockets](https://togithub.com/nestjs/nest) ([source](https://togithub.com/nestjs/nest/tree/HEAD/packages/websockets)) | [`10.3.10` -> `10.4.1`](https://renovatebot.com/diffs/npm/@nestjs%2fwebsockets/10.3.10/10.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fwebsockets/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fwebsockets/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fwebsockets/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fwebsockets/10.3.10/10.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@nx/vite](https://nx.dev) ([source](https://togithub.com/nrwl/nx/tree/HEAD/packages/vite)) | [`19.5.3` -> `19.6.0`](https://renovatebot.com/diffs/npm/@nx%2fvite/19.5.3/19.6.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nx%2fvite/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nx%2fvite/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nx%2fvite/19.5.3/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nx%2fvite/19.5.3/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@playwright/test](https://playwright.dev) ([source](https://togithub.com/microsoft/playwright)) | [`=1.44.1` -> `=1.46.1`](https://renovatebot.com/diffs/npm/@playwright%2ftest/1.44.1/1.46.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@playwright%2ftest/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@playwright%2ftest/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@playwright%2ftest/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@playwright%2ftest/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@prisma/client](https://www.prisma.io) ([source](https://togithub.com/prisma/prisma/tree/HEAD/packages/client)) | [`5.17.0` -> `5.18.0`](https://renovatebot.com/diffs/npm/@prisma%2fclient/5.17.0/5.18.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@prisma%2fclient/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@prisma%2fclient/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@prisma%2fclient/5.17.0/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@prisma%2fclient/5.17.0/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@prisma/instrumentation](https://www.prisma.io) ([source](https://togithub.com/prisma/prisma/tree/HEAD/packages/instrumentation)) | [`5.17.0` -> `5.18.0`](https://renovatebot.com/diffs/npm/@prisma%2finstrumentation/5.17.0/5.18.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@prisma%2finstrumentation/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@prisma%2finstrumentation/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@prisma%2finstrumentation/5.17.0/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@prisma%2finstrumentation/5.17.0/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@sentry/esbuild-plugin](https://togithub.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/esbuild-plugin) ([source](https://togithub.com/getsentry/sentry-javascript-bundler-plugins)) | [`2.21.1` -> `2.22.2`](https://renovatebot.com/diffs/npm/@sentry%2fesbuild-plugin/2.21.1/2.22.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@sentry%2fesbuild-plugin/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@sentry%2fesbuild-plugin/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@sentry%2fesbuild-plugin/2.21.1/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@sentry%2fesbuild-plugin/2.21.1/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@sentry/integrations](https://togithub.com/getsentry/sentry-javascript/tree/master/packages/integrations) ([source](https://togithub.com/getsentry/sentry-javascript)) | [`7.118.0` -> `7.119.0`](https://renovatebot.com/diffs/npm/@sentry%2fintegrations/7.118.0/7.119.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@sentry%2fintegrations/7.119.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@sentry%2fintegrations/7.119.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@sentry%2fintegrations/7.118.0/7.119.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@sentry%2fintegrations/7.118.0/7.119.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@sentry/react](https://togithub.com/getsentry/sentry-javascript/tree/master/packages/react) ([source](https://togithub.com/getsentry/sentry-javascript)) | [`8.20.0` -> `8.26.0`](https://renovatebot.com/diffs/npm/@sentry%2freact/8.20.0/8.26.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@sentry%2freact/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@sentry%2freact/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@sentry%2freact/8.20.0/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@sentry%2freact/8.20.0/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@sentry/react](https://togithub.com/getsentry/sentry-javascript/tree/master/packages/react) ([source](https://togithub.com/getsentry/sentry-javascript)) | [`8.20.0` -> `8.26.0`](https://renovatebot.com/diffs/npm/@sentry%2freact/8.20.0/8.26.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@sentry%2freact/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@sentry%2freact/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@sentry%2freact/8.20.0/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@sentry%2freact/8.20.0/8.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@sentry/webpack-plugin](https://togithub.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/webpack-plugin) ([source](https://togithub.com/getsentry/sentry-javascript-bundler-plugins)) | [`2.21.1` -> `2.22.2`](https://renovatebot.com/diffs/npm/@sentry%2fwebpack-plugin/2.21.1/2.22.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@sentry%2fwebpack-plugin/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@sentry%2fwebpack-plugin/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@sentry%2fwebpack-plugin/2.21.1/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@sentry%2fwebpack-plugin/2.21.1/2.22.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@swc/core](https://swc.rs) ([source](https://togithub.com/swc-project/swc)) | [`1.6.13` -> `1.7.12`](https://renovatebot.com/diffs/npm/@swc%2fcore/1.6.13/1.7.12) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@swc%2fcore/1.7.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@swc%2fcore/1.7.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@swc%2fcore/1.6.13/1.7.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@swc%2fcore/1.6.13/1.7.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@tanstack/react-table](https://tanstack.com/table) ([source](https://togithub.com/TanStack/table/tree/HEAD/packages/react-table)) | [`8.19.3` -> `8.20.1`](https://renovatebot.com/diffs/npm/@tanstack%2freact-table/8.19.3/8.20.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tanstack%2freact-table/8.20.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tanstack%2freact-table/8.20.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tanstack%2freact-table/8.19.3/8.20.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tanstack%2freact-table/8.19.3/8.20.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@toeverything/theme](https://togithub.com/toeverything/design) | [`1.0.4` -> `1.0.5`](https://renovatebot.com/diffs/npm/@toeverything%2ftheme/1.0.4/1.0.5) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@toeverything%2ftheme/1.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@toeverything%2ftheme/1.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@toeverything%2ftheme/1.0.4/1.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@toeverything%2ftheme/1.0.4/1.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [@types/node](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`20.14.12` -> `20.16.1`](https://renovatebot.com/diffs/npm/@types%2fnode/20.14.12/20.16.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/20.16.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@types%2fnode/20.16.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@types%2fnode/20.14.12/20.16.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/20.14.12/20.16.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@types/ws](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/ws) ([source](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws)) | [`8.5.11` -> `8.5.12`](https://renovatebot.com/diffs/npm/@types%2fws/8.5.11/8.5.12) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fws/8.5.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@types%2fws/8.5.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@types%2fws/8.5.11/8.5.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fws/8.5.11/8.5.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@vanilla-extract/css](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/css)) | [`1.15.3` -> `1.15.4`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fcss/1.15.3/1.15.4) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fcss/1.15.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fcss/1.15.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fcss/1.15.3/1.15.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fcss/1.15.3/1.15.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@vanilla-extract/dynamic](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/dynamic)) | [`2.1.1` -> `2.1.2`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fdynamic/2.1.1/2.1.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fdynamic/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fdynamic/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fdynamic/2.1.1/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fdynamic/2.1.1/2.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [@vanilla-extract/vite-plugin](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/vite-plugin)) | [`4.0.13` -> `4.0.14`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fvite-plugin/4.0.13/4.0.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fvite-plugin/4.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fvite-plugin/4.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fvite-plugin/4.0.13/4.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fvite-plugin/4.0.13/4.0.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@vanilla-extract/webpack-plugin](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/webpack-plugin)) | [`2.3.11` -> `2.3.12`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fwebpack-plugin/2.3.11/2.3.12) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fwebpack-plugin/2.3.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fwebpack-plugin/2.3.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fwebpack-plugin/2.3.11/2.3.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fwebpack-plugin/2.3.11/2.3.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [autoprefixer](https://togithub.com/postcss/autoprefixer) | [`10.4.19` -> `10.4.20`](https://renovatebot.com/diffs/npm/autoprefixer/10.4.19/10.4.20) | [![age](https://developer.mend.io/api/mc/badges/age/npm/autoprefixer/10.4.20?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/autoprefixer/10.4.20?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/autoprefixer/10.4.19/10.4.20?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/autoprefixer/10.4.19/10.4.20?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [commitlint](https://commitlint.js.org/) ([source](https://togithub.com/conventional-changelog/commitlint/tree/HEAD/@alias/commitlint)) | [`19.3.0` -> `19.4.0`](https://renovatebot.com/diffs/npm/commitlint/19.3.0/19.4.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/commitlint/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/commitlint/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/commitlint/19.3.0/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/commitlint/19.3.0/19.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [core-js](https://togithub.com/zloirock/core-js) ([source](https://togithub.com/zloirock/core-js/tree/HEAD/packages/core-js)) | [`3.37.1` -> `3.38.0`](https://renovatebot.com/diffs/npm/core-js/3.37.1/3.38.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/core-js/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/core-js/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/core-js/3.37.1/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/core-js/3.37.1/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [core-js](https://togithub.com/zloirock/core-js) ([source](https://togithub.com/zloirock/core-js/tree/HEAD/packages/core-js)) | [`3.37.1` -> `3.38.0`](https://renovatebot.com/diffs/npm/core-js/3.37.1/3.38.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/core-js/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/core-js/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/core-js/3.37.1/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/core-js/3.37.1/3.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [cssnano](https://togithub.com/cssnano/cssnano) | [`7.0.4` -> `7.0.5`](https://renovatebot.com/diffs/npm/cssnano/7.0.4/7.0.5) | [![age](https://developer.mend.io/api/mc/badges/age/npm/cssnano/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/cssnano/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/cssnano/7.0.4/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/cssnano/7.0.4/7.0.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [electron](https://togithub.com/electron/electron) | [`~30.2.0` -> `~30.4.0`](https://renovatebot.com/diffs/npm/electron/30.2.0/30.4.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/electron/30.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/electron/30.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/electron/30.2.0/30.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/electron/30.2.0/30.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [embla-carousel-react](https://www.embla-carousel.com) ([source](https://togithub.com/davidjerleke/embla-carousel)) | [`8.1.7` -> `8.1.8`](https://renovatebot.com/diffs/npm/embla-carousel-react/8.1.7/8.1.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/embla-carousel-react/8.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/embla-carousel-react/8.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/embla-carousel-react/8.1.7/8.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/embla-carousel-react/8.1.7/8.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [esbuild](https://togithub.com/evanw/esbuild) | [`0.23.0` -> `0.23.1`](https://renovatebot.com/diffs/npm/esbuild/0.23.0/0.23.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/esbuild/0.23.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/esbuild/0.23.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/esbuild/0.23.0/0.23.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/esbuild/0.23.0/0.23.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [file-type](https://togithub.com/sindresorhus/file-type) | [`19.3.0` -> `19.4.1`](https://renovatebot.com/diffs/npm/file-type/19.3.0/19.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/file-type/19.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/file-type/19.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/file-type/19.3.0/19.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/file-type/19.3.0/19.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [husky](https://togithub.com/typicode/husky) | [`9.1.2` -> `9.1.4`](https://renovatebot.com/diffs/npm/husky/9.1.2/9.1.4) | [![age](https://developer.mend.io/api/mc/badges/age/npm/husky/9.1.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/husky/9.1.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/husky/9.1.2/9.1.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/husky/9.1.2/9.1.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [i18next](https://www.i18next.com) ([source](https://togithub.com/i18next/i18next)) | [`23.12.2` -> `23.13.0`](https://renovatebot.com/diffs/npm/i18next/23.12.2/23.13.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/i18next/23.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/i18next/23.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/i18next/23.12.2/23.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/i18next/23.12.2/23.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [is-svg](https://togithub.com/sindresorhus/is-svg) | [`5.0.1` -> `5.1.0`](https://renovatebot.com/diffs/npm/is-svg/5.0.1/5.1.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/is-svg/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/is-svg/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/is-svg/5.0.1/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/is-svg/5.0.1/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [jotai](https://togithub.com/pmndrs/jotai) | [`2.9.1` -> `2.9.3`](https://renovatebot.com/diffs/npm/jotai/2.9.1/2.9.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai/2.9.1/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai/2.9.1/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [jotai](https://togithub.com/pmndrs/jotai) | [`2.9.1` -> `2.9.3`](https://renovatebot.com/diffs/npm/jotai/2.9.1/2.9.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai/2.9.1/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai/2.9.1/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [jotai-devtools](https://togithub.com/jotaijs/jotai-devtools) | [`0.10.0` -> `0.10.1`](https://renovatebot.com/diffs/npm/jotai-devtools/0.10.0/0.10.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai-devtools/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai-devtools/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai-devtools/0.10.0/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai-devtools/0.10.0/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [jotai-devtools](https://togithub.com/jotaijs/jotai-devtools) | [`0.10.0` -> `0.10.1`](https://renovatebot.com/diffs/npm/jotai-devtools/0.10.0/0.10.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai-devtools/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai-devtools/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai-devtools/0.10.0/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai-devtools/0.10.0/0.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [jotai-scope](https://togithub.com/jotaijs/jotai-scope) | [`0.7.0` -> `0.7.1`](https://renovatebot.com/diffs/npm/jotai-scope/0.7.0/0.7.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai-scope/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai-scope/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai-scope/0.7.0/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai-scope/0.7.0/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [lib0](https://togithub.com/dmonad/lib0) | [`0.2.95` -> `0.2.97`](https://renovatebot.com/diffs/npm/lib0/0.2.95/0.2.97) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lib0/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lib0/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lib0/0.2.95/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lib0/0.2.95/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [lib0](https://togithub.com/dmonad/lib0) | [`0.2.95` -> `0.2.97`](https://renovatebot.com/diffs/npm/lib0/0.2.95/0.2.97) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lib0/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lib0/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lib0/0.2.95/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lib0/0.2.95/0.2.97?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [lint-staged](https://togithub.com/lint-staged/lint-staged) | [`15.2.7` -> `15.2.9`](https://renovatebot.com/diffs/npm/lint-staged/15.2.7/15.2.9) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lint-staged/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lint-staged/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lint-staged/15.2.7/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lint-staged/15.2.7/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [lit](https://lit.dev/) ([source](https://togithub.com/lit/lit/tree/HEAD/packages/lit)) | [`3.1.4` -> `3.2.0`](https://renovatebot.com/diffs/npm/lit/3.1.4/3.2.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lit/3.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lit/3.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lit/3.1.4/3.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lit/3.1.4/3.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [lucide-react](https://lucide.dev) ([source](https://togithub.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react)) | [`^0.408.0` -> `^0.428.0`](https://renovatebot.com/diffs/npm/lucide-react/0.408.0/0.428.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-react/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lucide-react/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lucide-react/0.408.0/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-react/0.408.0/0.428.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [mixpanel-browser](https://togithub.com/mixpanel/mixpanel-js) | [`2.54.0` -> `2.55.0`](https://renovatebot.com/diffs/npm/mixpanel-browser/2.54.0/2.55.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/mixpanel-browser/2.55.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/mixpanel-browser/2.55.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/mixpanel-browser/2.54.0/2.55.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mixpanel-browser/2.54.0/2.55.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [msw](https://mswjs.io) ([source](https://togithub.com/mswjs/msw)) | [`2.3.4` -> `2.3.5`](https://renovatebot.com/diffs/npm/msw/2.3.4/2.3.5) | [![age](https://developer.mend.io/api/mc/badges/age/npm/msw/2.3.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/msw/2.3.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/msw/2.3.4/2.3.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/msw/2.3.4/2.3.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [napi](https://togithub.com/napi-rs/napi-rs) | `3.0.0-alpha.7` -> `3.0.0-alpha.8` | [![age](https://developer.mend.io/api/mc/badges/age/crate/napi/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi/3.0.0-alpha.7/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi/3.0.0-alpha.7/3.0.0-alpha.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [napi-derive](https://togithub.com/napi-rs/napi-rs) | `3.0.0-alpha.6` -> `3.0.0-alpha.7` | [![age](https://developer.mend.io/api/mc/badges/age/crate/napi-derive/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi-derive/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi-derive/3.0.0-alpha.6/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi-derive/3.0.0-alpha.6/3.0.0-alpha.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | workspace.dependencies | patch |
| [nest-commander](https://nest-commander.jaymcdoniel.dev) ([source](https://togithub.com/jmcdo29/nest-commander/tree/HEAD/pacakges/nest-commander)) | [`3.14.0` -> `3.15.0`](https://renovatebot.com/diffs/npm/nest-commander/3.14.0/3.15.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nest-commander/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nest-commander/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nest-commander/3.14.0/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nest-commander/3.14.0/3.15.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [nestjs-throttler-storage-redis](https://togithub.com/kkoomen/nestjs-throttler-storage-redis) | [`^0.4.1` -> `^0.5.0`](https://renovatebot.com/diffs/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nestjs-throttler-storage-redis/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nestjs-throttler-storage-redis/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nestjs-throttler-storage-redis/0.4.4/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [node](https://nodejs.org) ([source](https://togithub.com/nodejs/node)) | `20.15.1` -> `20.16.0` | [![age](https://developer.mend.io/api/mc/badges/age/node-version/node/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/node-version/node/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/node-version/node/v20.15.1/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/node-version/node/v20.15.1/v20.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |  | minor |
| [nx](https://nx.dev) ([source](https://togithub.com/nrwl/nx/tree/HEAD/packages/nx)) | [`19.5.3` -> `19.6.0`](https://renovatebot.com/diffs/npm/nx/19.5.3/19.6.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/nx/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/nx/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/nx/19.5.3/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nx/19.5.3/19.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [openai](https://togithub.com/openai/openai-node) | [`4.53.2` -> `4.56.0`](https://renovatebot.com/diffs/npm/openai/4.53.2/4.56.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/openai/4.56.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/openai/4.56.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/openai/4.53.2/4.56.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/openai/4.53.2/4.56.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| openresty/openresty | `1.25.3.1-0-buster` -> `1.25.3.2-0-buster` | [![age](https://developer.mend.io/api/mc/badges/age/docker/openresty%2fopenresty/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/docker/openresty%2fopenresty/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/docker/openresty%2fopenresty/1.25.3.1/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/docker/openresty%2fopenresty/1.25.3.1/1.25.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | final | patch |
| [playwright](https://playwright.dev) ([source](https://togithub.com/microsoft/playwright)) | [`=1.44.1` -> `=1.46.1`](https://renovatebot.com/diffs/npm/playwright/1.44.1/1.46.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/playwright/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/playwright/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/playwright/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/playwright/1.44.1/1.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [postcss](https://postcss.org/) ([source](https://togithub.com/postcss/postcss)) | [`8.4.40` -> `8.4.41`](https://renovatebot.com/diffs/npm/postcss/8.4.40/8.4.41) | [![age](https://developer.mend.io/api/mc/badges/age/npm/postcss/8.4.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/postcss/8.4.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/postcss/8.4.40/8.4.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/postcss/8.4.40/8.4.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [prisma](https://www.prisma.io) ([source](https://togithub.com/prisma/prisma/tree/HEAD/packages/cli)) | [`5.17.0` -> `5.18.0`](https://renovatebot.com/diffs/npm/prisma/5.17.0/5.18.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/prisma/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/prisma/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/prisma/5.17.0/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/prisma/5.17.0/5.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [react-hook-form](https://www.react-hook-form.com) ([source](https://togithub.com/react-hook-form/react-hook-form)) | [`7.52.1` -> `7.52.2`](https://renovatebot.com/diffs/npm/react-hook-form/7.52.1/7.52.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-hook-form/7.52.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-hook-form/7.52.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-hook-form/7.52.1/7.52.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-hook-form/7.52.1/7.52.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [react-i18next](https://togithub.com/i18next/react-i18next) | [`15.0.0` -> `15.0.1`](https://renovatebot.com/diffs/npm/react-i18next/15.0.0/15.0.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-i18next/15.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-i18next/15.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-i18next/15.0.0/15.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-i18next/15.0.0/15.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | patch |
| [react-refresh](https://reactjs.org/) ([source](https://togithub.com/facebook/react/tree/HEAD/packages/react)) | [`^0.10.0` -> `^0.14.0`](https://renovatebot.com/diffs/npm/react-refresh/0.10.0/0.14.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-refresh/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-refresh/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-refresh/0.10.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-refresh/0.10.0/0.14.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [react-resizable-panels](https://togithub.com/bvaughn/react-resizable-panels) | [`2.0.22` -> `2.1.0`](https://renovatebot.com/diffs/npm/react-resizable-panels/2.0.22/2.1.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-resizable-panels/2.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-resizable-panels/2.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-resizable-panels/2.0.22/2.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-resizable-panels/2.0.22/2.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [react-router-dom](https://togithub.com/remix-run/react-router) ([source](https://togithub.com/remix-run/react-router/tree/HEAD/packages/react-router-dom)) | [`6.25.1` -> `6.26.1`](https://renovatebot.com/diffs/npm/react-router-dom/6.25.1/6.26.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-router-dom/6.26.1?slim=true)](https://docs.renovatebot.com/merge
2024-08-19 07:51:51 +00:00
pengx17
4a89b1a5dd fix(electron): adjust tab styles (#7919)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/121ee595-99f0-43cb-be5f-ca1bdd15d6ca.png)

fix AF-1261
2024-08-19 06:15:06 +00:00
JimmFly
4916eea24f feat(core): new share menu (#7838)
close AF-1224 AF-1216

![image](https://github.com/user-attachments/assets/204c408a-3dab-4068-86f6-e20abcfa863c)
2024-08-19 05:51:05 +00:00
EYHN
cfac3ebf1f feat(core): workbench open doc in mode (#7906) 2024-08-19 02:37:33 +00:00
pengx17
e0a91f63d3 fix(core): tag menu should not accept keyboard shortcut when renaming tag (#7913)
fix #7890, fix PD-1603
2024-08-18 08:29:25 +00:00
pengx17
23c73243ab chore(electron): re-enable sentry for electron (#7898)
related
https://github.com/toeverything/AFFiNE/pull/7001
2024-08-17 04:32:49 +00:00
renovate[bot]
f324fa4719 chore: bump up storybook monorepo to v8 (major) (#6068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: EYHN <cneyhn@gmail.com>
2024-08-16 11:42:24 +00:00
EYHN
c822594882 feat(core): mode in query string (#7904) 2024-08-16 10:59:43 +00:00
EYHN
83716c2fd9 feat(core): share in workspace link (#7897)
ShareDocsService -> ShareDocsListService
ShareService -> ShareInfoService
(*new) ShareReaderService

`/share/:workspaceId/:docId` -> redirect to -> `/workspace/:workspaceId/:docId`

workspace loading process

1. find workspace in workspace list
2. (if not found) revalidate workspace list
3. (if still not found) try load share page
4. (if share page found) => share page
5. (if share page not found) => 404
6. (if workspace found) => workspace page
2024-08-16 09:12:18 +00:00
L-Sun
620d20710a chore: bump bs (#7899)
Bump blocksuite from https://github.com/toeverything/blocksuite/pull/7934

## Bugfix
- https://github.com/toeverything/BlockSuite/pull/7985 @L-Sun

## Misc
- https://github.com/toeverything/BlockSuite/pull/7971 @L-Sun
- https://github.com/toeverything/BlockSuite/pull/7965 @doouding
2024-08-16 08:46:16 +00:00
CatsJuice
cfc367efe7 feat(component): add more customizable prop for radio-group with new story (#7850)
![CleanShot 2024-08-13 at 16.40.22@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/06a128c3-f0bb-47f9-babf-6d4eca2597d7.png)
2024-08-16 07:19:16 +00:00
pengx17
69c507fded fix(electron): do not use async callback in onBeforeSendHeaders (#7894)
it seems using async callback for onBeforeSendHeaders may crash the app.
the main reason why it is async is to read the cookies from session.

fix AF-1172
2024-08-16 05:17:47 +00:00
pengx17
b57ce4646f chore(electron): adjust log level (#7887) 2024-08-16 05:17:46 +00:00
Brooooooklyn
dba024d561 docs: update LICENSE description (#7869) 2024-08-15 14:29:07 +00:00
darkskygit
e26ba48a45 feat: update ingress health check (#7888) 2024-08-15 12:16:25 +00:00
forehalo
e53dde7944 test(server): all instance variants (#7882) 2024-08-15 10:01:54 +00:00
forehalo
624f3514fc feat(server): make permission a standalone module (#7880) 2024-08-15 10:01:52 +00:00
donteatfriedrice
ba8958f39b fix: chat block peek view input reset height (#7884)
[BS-1202](https://linear.app/affine-design/issue/BS-1202/ai-chat-block-输入大段内容发送后输入框-resize-问题)
2024-08-15 08:08:00 +00:00
pengx17
9af0e53ae2 fix(electron): header button not working on linux (#7883)
tested locally on windows/linux(vm)
2024-08-15 04:39:47 +00:00
forehalo
5a2f93f035 chore(admin): disable ai settings (#7877) 2024-08-15 02:50:46 +00:00
forehalo
9192ac4420 fix(server): command line nestjs does not have http adapter (#7876) 2024-08-14 13:41:23 +00:00
liuyi
57449c1530 fix(server): redirect to setup page if not initialized (#7875) 2024-08-14 21:02:16 +08:00
EYHN
89537e6892 refactor(core): separate editor & doc mode (#7873)
doc.mode -> primaryMode
(*new) editor.mode

New Service:
editor service

Change Mode:

```
const editor = useService(EditorService).editor;
editor.setMode('page')
```

Change primary mode

```
const editor = useService(EditorService).editor;
editor.doc.setPrimaryMode('page')
```
2024-08-14 11:43:03 +00:00
EYHN
50948318e0 feat(core): use emoji as folder icon (#7842)
![CleanShot 2024-08-12 at 22.33.42.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/g3jz87HxbjOJpXV3FPT7/c0c1a196-be29-4e45-a28d-87a55d80fc9d.png)
2024-08-14 10:35:22 +00:00
EYHN
0504d0b0ff feat(core): init feature flag service (#7856) 2024-08-14 10:35:21 +00:00
darkskygit
339c39c1ec feat: improve prompt management (#7853) 2024-08-14 08:38:36 +00:00
forehalo
cd3924b8fc Revert "fix(server): redirect to setup page if not initialized (#7871)"
This reverts commit 42b5ef7a8b.
2024-08-14 16:37:52 +08:00
liuyi
42b5ef7a8b fix(server): redirect to setup page if not initialized (#7871) 2024-08-14 08:05:55 +00:00
donteatfriedrice
ad42418089 fix: add hover enter delay for ask ai button (#7872)
[AF-1075](https://linear.app/affine-design/issue/AF-1075/双击选中内容的时候,如果-toolbar-出现在内容上方,ai-action-会自动展开)
2024-08-14 07:27:17 +00:00
forehalo
99e70c91e8 perf(core): avoid page init when only id required (#7867) 2024-08-14 05:22:18 +00:00
doouding
05ac3dbdcb feat: bump bs (#7866) 2024-08-14 04:42:20 +00:00
JimmFly
994b539507 fix(admin): user form not dynamically updating as expected (#7858)
- Fixed the issue that a certain feature must be enabled when creating a user
- Fixed the issue that the modified content was not reset to the default content when exiting the user form without saving after modification
- Fixed the issue that the content did not switch as expected when switching user forms of different users

https://github.com/user-attachments/assets/02567021-9342-4ed1-be77-3bdcbb3d86ab
2024-08-14 04:28:15 +00:00
pengx17
d5edadabe6 fix(electron): cmd+num not working on mac (#7865)
fix AF-1248
hidden menu group + acceleratorWorksWhenHidden does not work on mac
2024-08-14 04:14:16 +00:00
JimmFly
05247bb24e fix(admin): frequent query requests in the search (#7854) 2024-08-14 03:49:45 +00:00
forehalo
f69f026ac3 fix(admin): avoid frequent refetch (#7863) 2024-08-14 03:34:41 +00:00
forehalo
015247345c chore(admin): organize massive routes (#7857) 2024-08-14 03:34:38 +00:00
forehalo
0ba516866f fix(server): change password with token should be public (#7855) 2024-08-14 03:34:35 +00:00
forehalo
7afba6b8b5 fix(server): prelude should load both local and remote config file (#7852) 2024-08-14 03:34:33 +00:00
renovate
08cc15a55c chore: bump up oxlint version to v0.7.1 (#7846)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [oxlint](https://oxc.rs) ([source](https://togithub.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`0.7.0` -> `0.7.1`](https://renovatebot.com/diffs/npm/oxlint/0.7.0/0.7.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxlint/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxlint/0.7.0/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/0.7.0/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v0.7.1`](https://togithub.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#071---2024-08-12)

[Compare Source](3ac02fd838...972492cc4d)

##### Features

-   [`cc922f4`](https://togithub.com/oxc-project/oxc/commit/cc922f4) vscode: Provide config's schema to oxlint config files ([#&#8203;4826](https://togithub.com/oxc-project/oxc/issues/4826)) (Don Isaac)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yMC4xIiwidXBkYXRlZEluVmVyIjoiMzguMjAuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-13 14:02:00 +00:00
DarkSky
24c34eb3fc fix: admin panel schema (#7851) 2024-08-13 09:16:56 +00:00
JimmFly
ba5ba71f35 chore: add test for all collection and all tag (#7687) 2024-08-13 08:19:54 +00:00
pengx17
d4065fee78 fix(electron): adjust app-tabs-header styles (#7849) 2024-08-13 08:06:30 +00:00
pengx17
d86f7f41dc fix: center peek support open in new tab (#7848) 2024-08-13 07:52:03 +00:00
655 changed files with 22084 additions and 15185 deletions

36
.github/actions/cluster-auth/action.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: 'Auth to Cluster'
description: 'Auth to the GCP Cluster'
inputs:
gcp-project-number:
description: 'GCP project number'
required: true
gcp-project-id:
description: 'GCP project id'
required: true
service-account:
description: 'Service account'
cluster-name:
description: 'Cluster name'
cluster-location:
description: 'Cluster location'
runs:
using: 'composite'
steps:
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/${{ inputs.gcp-project-number }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions-helm-deploy'
service_account: '${{ inputs.service-account }}'
token_format: 'access_token'
project_id: '${{ inputs.gcp-project-id }}'
- name: 'Setup gcloud cli'
uses: 'google-github-actions/setup-gcloud@v2'
with:
install_components: 'gke-gcloud-auth-plugin'
- id: get-gke-credentials
shell: bash
run: |
gcloud container clusters get-credentials ${{ inputs.cluster-name }} --region ${{ inputs.cluster-location }} --project ${{ inputs.gcp-project-id }}

View File

@@ -24,24 +24,14 @@ runs:
shell: bash
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
- uses: azure/setup-helm@v4
- id: auth
uses: google-github-actions/auth@v2
- name: 'Auth to cluster'
uses: './.github/actions/cluster-auth'
with:
workload_identity_provider: 'projects/${{ inputs.gcp-project-number }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions-helm-deploy'
service_account: '${{ inputs.service-account }}'
token_format: 'access_token'
project_id: '${{ inputs.gcp-project-id }}'
- name: 'Setup gcloud cli'
uses: 'google-github-actions/setup-gcloud@v2'
with:
install_components: 'gke-gcloud-auth-plugin'
- id: get-gke-credentials
shell: bash
run: |
gcloud container clusters get-credentials ${{ inputs.cluster-name }} --region ${{ inputs.cluster-location }} --project ${{ inputs.gcp-project-id }}
gcp-project-number: '${{ inputs.gcp-project-number }}'
gcp-project-id: '${{ inputs.gcp-project-id }}'
service-account: '${{ inputs.service-account }}'
cluster-name: '${{ inputs.cluster-name }}'
cluster-location: '${{ inputs.cluster-location }}'
- name: Deploy
shell: bash

View File

@@ -1,4 +1,4 @@
FROM openresty/openresty:1.25.3.1-0-buster
FROM openresty/openresty:1.25.3.2-0-buster
WORKDIR /app
COPY ./packages/frontend/web/dist ./dist
COPY ./packages/frontend/admin/dist ./admin

View File

@@ -204,12 +204,12 @@ spec:
protocol: TCP
livenessProbe:
httpGet:
path: /
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "graphql.fullname" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:

View File

@@ -36,14 +36,14 @@ graphql:
type: ClusterIP
port: 3000
annotations:
cloud.google.com/backend-config: '{"default": "affine-backendconfig"}'
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
sync:
service:
type: ClusterIP
port: 3010
annotations:
cloud.google.com/backend-config: '{"default": "affine-backendconfig"}'
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
service:

View File

@@ -0,0 +1,10 @@
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: "affine-api-backendconfig"
spec:
healthCheck:
timeoutSec: 1
type: HTTP
requestPath: /info

View File

@@ -180,7 +180,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
electron-install: true
full-cache: true
- name: Download affine.linux-x64-gnu.node

View File

@@ -21,6 +21,47 @@ permissions:
packages: 'write'
jobs:
output-prev-version:
name: Output previous version
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
outputs:
prev: ${{ steps.print.outputs.version }}
namespace: ${{ steps.print.outputs.namespace }}
steps:
- uses: actions/checkout@v4
- name: Auth to Cluster
uses: './.github/actions/cluster-auth'
with:
gcp-project-number: ${{ secrets.GCP_PROJECT_NUMBER }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
service-account: ${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }}
cluster-name: ${{ secrets.GCP_CLUSTER_NAME }}
cluster-location: ${{ secrets.GCP_CLUSTER_LOCATION }}
- name: Output previous version
id: print
run: |
namespace=""
if [ "${{ github.event.inputs.flavor }}" = "canary" ]; then
namespace="dev"
elif [ "${{ github.event.inputs.flavor }}" = "beta" ]; then
namespace="beta"
elif [ "${{ github.event.inputs.flavor }}" = "stable" ]; then
namespace="production"
else
echo "Invalid flavor: ${{ github.event.inputs.flavor }}"
exit 1
fi
echo "Namespace set to: $namespace"
# Get the previous version from the deployment
prev_version=$(kubectl get deployment -n $namespace affine-graphql -o=jsonpath='{.spec.template.spec.containers[0].image}' | awk -F '-' '{print $3}')
echo "Previous version: $prev_version"
echo "version=$prev_version" >> $GITHUB_OUTPUT
echo "namesapce=$namespace" >> $GITHUB_OUTPUT
build-server-image:
name: Build Server Image
uses: ./.github/workflows/build-server-image.yml
@@ -193,3 +234,95 @@ jobs:
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
STRIPE_WEBHOOK_KEY: ${{ secrets.STRIPE_WEBHOOK_KEY }}
STATIC_IP_NAME: ${{ secrets.STATIC_IP_NAME }}
deploy-done:
needs:
- output-prev-version
- build-frontend-image
- build-server-image
- deploy
if: always()
runs-on: ubuntu-latest
name: Post deploy message
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
repository: toeverything/blocksuite
path: blocksuite
fetch-depth: 0
fetch-tags: true
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: 'workspaces focus @affine/changelog'
electron-install: false
- name: Output deployed info
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
id: set_info
run: |
if [ "${{ github.event.inputs.flavor }}" = "canary" ]; then
echo "deployed_url=https://affine.fail" >> $GITHUB_OUTPUT
elif [ "${{ github.event.inputs.flavor }}" = "beta" ]; then
echo "deployed_url=https://insider.affine.pro" >> $GITHUB_OUTPUT
elif [ "${{ github.event.inputs.flavor }}" = "stable" ]; then
echo "deployed_url=https://app.affine.pro" >> $GITHUB_OUTPUT
else
exit 1
fi
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Post Success event to a Slack channel
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
run: node ./tools/changelog/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
DEPLOYED_URL: ${{ steps.set_info.outputs.deployed_url }}
PREV_VERSION: ${{ needs.output-prev-version.outputs.prev }}
NAMESPACE: ${{ needs.output-prev-version.outputs.namespace }}
DEPLOYMENT: 'SERVER'
FLAVOR: ${{ github.event.inputs.flavor }}
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
- name: Post Failed event to a Slack channel
id: failed-slack
uses: slackapi/slack-github-action@v1.26.0
if: ${{ always() && contains(needs.*.result, 'failure') }}
with:
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>",
"type": "mrkdwn"
}
}
]
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Post Cancel event to a Slack channel
id: cancel-slack
uses: slackapi/slack-github-action@v1.26.0
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
with:
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>",
"type": "mrkdwn"
}
}
]
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

2
.nvmrc
View File

@@ -1 +1 @@
20.15.1
20.17.0

File diff suppressed because one or more lines are too long

925
.yarn/releases/yarn-4.4.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -12,4 +12,4 @@ npmPublishAccess: public
npmPublishRegistry: "https://registry.npmjs.org"
yarnPath: .yarn/releases/yarn-4.3.1.cjs
yarnPath: .yarn/releases/yarn-4.4.0.cjs

262
Cargo.lock generated
View File

@@ -155,6 +155,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
@@ -243,9 +249,12 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cc"
version = "1.0.100"
version = "1.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b"
checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
@@ -267,6 +276,15 @@ dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -353,7 +371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [
"quote",
"syn 2.0.68",
"syn",
]
[[package]]
@@ -388,7 +406,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
]
[[package]]
@@ -453,9 +471,14 @@ dependencies = [
[[package]]
name = "event-listener"
version = "2.5.3"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "fancy-regex"
@@ -660,21 +683,18 @@ dependencies = [
[[package]]
name = "hashlink"
version = "0.8.4"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
dependencies = [
"unicode-segmentation",
]
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
@@ -875,9 +895,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.27.0"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
@@ -982,6 +1002,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
"wasi",
"windows-sys 0.52.0",
]
[[package]]
name = "nanoid"
version = "0.4.0"
@@ -993,9 +1025,9 @@ dependencies = [
[[package]]
name = "napi"
version = "3.0.0-alpha.7"
version = "3.0.0-alpha.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec04344cc540f5897e97c9821ab99e7eb276b4dca6f3e6e441dfa72e5bcde70"
checksum = "743b5a7769f54c95e20a26d9e66d1b43d5622b7dc8ec8f97b51ed8c58633841f"
dependencies = [
"anyhow",
"bitflags 2.5.0",
@@ -1016,23 +1048,23 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
[[package]]
name = "napi-derive"
version = "3.0.0-alpha.6"
version = "3.0.0-alpha.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c6240c4ddca592cde608bbfa26e2af397c3596e413a0c65c9bbcb65c2f1e485"
checksum = "c7619cfcc3985e1ed73d147d6950caabaedabcf5c98133502f9d18c3d0061320"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "2.0.0-alpha.6"
version = "2.0.0-alpha.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32dcc50065508fe2f387076c17adbdf10e038d1c080d48b10196813d94ac6a8"
checksum = "584f6a91c05e8c6bf80622fcc2675c7d27934754d4f1141cfd422d531a3f51fb"
dependencies = [
"convert_case",
"once_cell",
@@ -1040,7 +1072,7 @@ dependencies = [
"quote",
"regex",
"semver",
"syn 2.0.68",
"syn",
]
[[package]]
@@ -1076,7 +1108,7 @@ dependencies = [
"kqueue",
"libc",
"log",
"mio",
"mio 0.8.11",
"serde",
"walkdir",
"windows-sys 0.48.0",
@@ -1139,16 +1171,6 @@ dependencies = [
"libm",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "object"
version = "0.36.0"
@@ -1180,6 +1202,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -1457,31 +1485,42 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.12"
version = "0.23.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"sct",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
dependencies = [
"base64",
"base64 0.22.1",
"rustls-pki-types",
]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
name = "rustls-pki-types"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
[[package]]
name = "rustls-webpki"
version = "0.102.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
@@ -1518,16 +1557,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "semver"
version = "1.0.23"
@@ -1536,30 +1565,43 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.204"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.204"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.120"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
@@ -1606,6 +1648,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@@ -1639,6 +1687,9 @@ name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
dependencies = [
"serde",
]
[[package]]
name = "smol_str"
@@ -1690,9 +1741,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
checksum = "fcfa89bea9500db4a0d038513d7a060566bfc51d46d1c014847049a45cce85e8"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -1703,11 +1754,10 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
checksum = "d06e2f2bd861719b1f3f0c7dbe1d80c30bf59e76cf019f07d9014ed7eefb8e08"
dependencies = [
"ahash",
"atoi",
"byteorder",
"bytes",
@@ -1721,6 +1771,7 @@ dependencies = [
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.14.5",
"hashlink",
"hex",
"indexmap",
@@ -1746,22 +1797,22 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
checksum = "2f998a9defdbd48ed005a89362bd40dd2117502f15294f61c8d47034107dbbdc"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 1.0.109",
"syn",
]
[[package]]
name = "sqlx-macros-core"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce"
dependencies = [
"dotenvy",
"either",
@@ -1777,7 +1828,7 @@ dependencies = [
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 1.0.109",
"syn",
"tempfile",
"tokio",
"url",
@@ -1785,12 +1836,12 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
checksum = "936cac0ab331b14cb3921c62156d913e4c15b74fb6ec0f3146bd4ef6e4fb3c12"
dependencies = [
"atoi",
"base64",
"base64 0.22.1",
"bitflags 2.5.0",
"byteorder",
"bytes",
@@ -1828,12 +1879,12 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
checksum = "9734dbce698c67ecf67c442f768a5e90a49b2a4d61a9f1d59f73874bd4cf0710"
dependencies = [
"atoi",
"base64",
"base64 0.22.1",
"bitflags 2.5.0",
"byteorder",
"chrono",
@@ -1867,9 +1918,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
checksum = "a75b419c3c1b1697833dd927bdc4c6545a620bc1bbafabd44e1efbe9afcd337e"
dependencies = [
"atoi",
"chrono",
@@ -1883,10 +1934,10 @@ dependencies = [
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"tracing",
"url",
"urlencoding",
]
[[package]]
@@ -1906,17 +1957,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.68"
@@ -1963,7 +2003,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
]
[[package]]
@@ -1983,7 +2023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c314e7ce51440f9e8f5a497394682a57b7c323d0f4d0a6b1b13c429056e0e234"
dependencies = [
"anyhow",
"base64",
"base64 0.21.7",
"bstr",
"fancy-regex",
"lazy_static",
@@ -2008,32 +2048,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.38.0"
version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"mio 1.0.2",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
]
[[package]]
@@ -2067,7 +2106,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
]
[[package]]
@@ -2171,12 +2210,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "uuid"
version = "1.10.0"
@@ -2249,7 +2282,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
"wasm-bindgen-shared",
]
@@ -2271,7 +2304,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -2284,9 +2317,12 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "webpki-roots"
version = "0.25.4"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
@@ -2556,7 +2592,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
"syn",
]
[[package]]

View File

@@ -18,7 +18,7 @@ rand = "0.8"
serde = "1"
serde_json = "1"
sha3 = "0.10"
sqlx = { version = "0.7", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
tiktoken-rs = "0.5"
tokio = "1.37"
uuid = "1.8"

View File

@@ -176,6 +176,12 @@ Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testi
## License
### Editions
- AFFiNE Community Edition (CE) is the current available version, it's free for self-host under the MIT license.
- AFFiNE Enterprise Edition (EE) is yet to be published, it will have more advanced features and enterprise-oriented offerings, including but not exclusive to rebranding and SSO, advanced admin and audit, etc., you may refer to https://affine.pro/pricing for more information
See [LICENSE] for details.
[all-contributors-badge]: https://img.shields.io/github/contributors/toeverything/AFFiNE

View File

@@ -60,7 +60,7 @@
"@istanbuljs/schema": "^0.1.3",
"@magic-works/i18n-codegen": "^0.6.0",
"@nx/vite": "^19.5.3",
"@playwright/test": "=1.44.1",
"@playwright/test": "=1.46.1",
"@taplo/cli": "^0.7.0",
"@testing-library/react": "^16.0.0",
"@toeverything/infra": "workspace:*",
@@ -75,7 +75,7 @@
"@vitest/coverage-istanbul": "1.6.0",
"@vitest/ui": "1.6.0",
"cross-env": "^7.0.3",
"electron": "~30.2.0",
"electron": "^32.0.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import-x": "^0.5.0",
@@ -88,15 +88,15 @@
"eslint-plugin-unused-imports": "^3.1.0",
"eslint-plugin-vue": "^9.24.1",
"fake-indexeddb": "6.0.0",
"happy-dom": "^14.7.1",
"happy-dom": "^15.0.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"msw": "^2.3.0",
"nanoid": "^5.0.7",
"nx": "^19.0.0",
"nyc": "^17.0.0",
"oxlint": "0.7.0",
"prettier": "^3.2.5",
"oxlint": "0.8.0",
"prettier": "^3.3.3",
"semver": "^7.6.0",
"serve": "^14.2.1",
"string-width": "^7.1.0",
@@ -110,7 +110,7 @@
"vitest-fetch-mock": "^0.3.0",
"vitest-mock-extended": "^2.0.0"
},
"packageManager": "yarn@4.3.1",
"packageManager": "yarn@4.4.0",
"resolutions": {
"array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@latest",
"array-includes": "npm:@nolyfill/array-includes@latest",

View File

@@ -33,7 +33,7 @@
"build:debug": "napi build"
},
"devDependencies": {
"@napi-rs/cli": "3.0.0-alpha.60",
"@napi-rs/cli": "3.0.0-alpha.62",
"lib0": "^0.2.93",
"nx": "^19.0.0",
"nx-cloud": "^19.0.0",

View File

@@ -1 +1,2 @@
.env
static/

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "ai_prompts_metadata" ADD COLUMN "modified" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "user_snapshots" (
"user_id" VARCHAR NOT NULL,
"id" VARCHAR NOT NULL,
"blob" BYTEA NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "user_snapshots_pkey" PRIMARY KEY ("user_id","id")
);
-- AddForeignKey
ALTER TABLE "user_snapshots" ADD CONSTRAINT "user_snapshots_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,14 @@
/*
Warnings:
- The primary key for the `updates` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- AlterTable
ALTER TABLE "snapshots" ALTER COLUMN "seq" DROP NOT NULL;
-- AlterTable
ALTER TABLE "updates" DROP CONSTRAINT "updates_pkey",
ALTER COLUMN "created_at" DROP DEFAULT,
ALTER COLUMN "seq" DROP NOT NULL,
ADD CONSTRAINT "updates_pkey" PRIMARY KEY ("workspace_id", "guid", "created_at");

View File

@@ -21,11 +21,11 @@
"dependencies": {
"@apollo/server": "^4.10.2",
"@aws-sdk/client-s3": "^3.620.0",
"@fal-ai/serverless-client": "^0.13.0",
"@fal-ai/serverless-client": "^0.14.0",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.19.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.2.0",
"@google-cloud/opentelemetry-resource-util": "^2.2.0",
"@keyv/redis": "^2.8.4",
"@keyv/redis": "^3.0.0",
"@nestjs/apollo": "^12.1.0",
"@nestjs/common": "^10.3.7",
"@nestjs/core": "^10.3.7",
@@ -34,8 +34,7 @@
"@nestjs/platform-express": "^10.3.7",
"@nestjs/platform-socket.io": "^10.3.7",
"@nestjs/schedule": "^4.0.1",
"@nestjs/serve-static": "^4.0.2",
"@nestjs/throttler": "5.2.0",
"@nestjs/throttler": "6.2.1",
"@nestjs/websockets": "^10.3.7",
"@node-rs/argon2": "^1.8.0",
"@node-rs/crc32": "^1.10.0",
@@ -77,7 +76,7 @@
"mustache": "^4.2.0",
"nanoid": "^5.0.7",
"nest-commander": "^3.12.5",
"nestjs-throttler-storage-redis": "^0.4.1",
"nestjs-throttler-storage-redis": "^0.5.0",
"nodemailer": "^6.9.13",
"on-headers": "^1.0.2",
"openai": "^4.33.0",
@@ -138,6 +137,7 @@
],
"watchMode": {
"ignoreChanges": [
"static/**",
"**/*.gen.*"
]
},

View File

@@ -32,6 +32,7 @@ model User {
sessions UserSession[]
aiSessions AiSession[]
updatedRuntimeConfigs RuntimeConfig[]
userSnapshots UserSnapshot[]
@@index([email])
@@map("users")
@@ -235,25 +236,49 @@ model Snapshot {
workspaceId String @map("workspace_id") @db.VarChar
id String @default(uuid()) @map("guid") @db.VarChar
blob Bytes @db.ByteA
seq Int @default(0) @db.Integer
state Bytes? @db.ByteA
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
// the `updated_at` field will not record the time of record changed,
// but the created time of last seen update that has been merged into snapshot.
updatedAt DateTime @map("updated_at") @db.Timestamptz(3)
// @deprecated use updatedAt only
seq Int? @default(0) @db.Integer
// we need to clear all hanging updates and snapshots before enable the foreign key on workspaceId
// workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@id([id, workspaceId])
@@map("snapshots")
}
// user snapshots are special snapshots for user storage like personal app settings, distinguished from workspace snapshots
// basically they share the same structure with workspace snapshots
// but for convenience, we don't fork the updates queue and hisotry for user snapshots, until we have to
// which means all operation on user snapshot will happen in-pace
model UserSnapshot {
userId String @map("user_id") @db.VarChar
id String @map("id") @db.VarChar
blob Bytes @db.ByteA
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, id])
@@map("user_snapshots")
}
model Update {
workspaceId String @map("workspace_id") @db.VarChar
id String @map("guid") @db.VarChar
seq Int @db.Integer
blob Bytes @db.ByteA
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
createdAt DateTime @map("created_at") @db.Timestamptz(3)
@@id([workspaceId, id, seq])
// @deprecated use createdAt only
seq Int? @db.Integer
@@id([workspaceId, id, createdAt])
@@map("updates")
}
@@ -367,6 +392,9 @@ model AiPrompt {
model String @db.VarChar
config Json? @db.Json
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
// whether the prompt is modified by the admin panel
modified Boolean @default(false)
messages AiPromptMessage[]
sessions AiSession[]

View File

@@ -3,7 +3,7 @@ import { Controller, Get } from '@nestjs/common';
import { Public } from './core/auth';
import { Config, SkipThrottle } from './fundamentals';
@Controller('/')
@Controller('/info')
export class AppController {
constructor(private readonly config: Config) {}
@@ -15,7 +15,7 @@ export class AppController {
compatibility: this.config.version,
message: `AFFiNE ${this.config.version} Server`,
type: this.config.type,
flavor: this.config.flavor,
flavor: this.config.flavor.type,
};
}
}

View File

@@ -1,5 +1,3 @@
import { join } from 'node:path';
import {
DynamicModule,
ForwardReference,
@@ -7,16 +5,16 @@ import {
Module,
} from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { get } from 'lodash-es';
import { AppController } from './app.controller';
import { AuthModule } from './core/auth';
import { ADD_ENABLED_FEATURES, ServerConfigModule } from './core/config';
import { DocModule } from './core/doc';
import { DocStorageModule } from './core/doc';
import { FeatureModule } from './core/features';
import { PermissionModule } from './core/permission';
import { QuotaModule } from './core/quota';
import { CustomSetupModule } from './core/setup';
import { SelfhostModule } from './core/selfhost';
import { StorageModule } from './core/storage';
import { SyncModule } from './core/sync';
import { UserModule } from './core/user';
@@ -137,7 +135,7 @@ export class AppModuleBuilder {
compile() {
@Module({
imports: this.modules,
controllers: this.config.isSelfhosted ? [] : [AppController],
controllers: [AppController],
})
class AppModule {}
@@ -145,20 +143,20 @@ export class AppModuleBuilder {
}
}
function buildAppModule() {
export function buildAppModule() {
AFFiNE = mergeConfigOverride(AFFiNE);
const factor = new AppModuleBuilder(AFFiNE);
factor
// common fundamental modules
// basic
.use(...FunctionalityModules)
.useIf(config => config.flavor.sync, WebSocketModule)
// auth
.use(UserModule, AuthModule)
.use(UserModule, AuthModule, PermissionModule)
// business modules
.use(DocModule)
.use(DocStorageModule)
// sync server only
.useIf(config => config.flavor.sync, SyncModule)
@@ -166,27 +164,16 @@ function buildAppModule() {
// graphql server only
.useIf(
config => config.flavor.graphql,
ServerConfigModule,
GqlModule,
StorageModule,
ServerConfigModule,
WorkspaceModule,
FeatureModule,
QuotaModule
)
// self hosted server only
.useIf(
config => config.isSelfhosted,
CustomSetupModule,
ServeStaticModule.forRoot({
rootPath: join('/app', 'static'),
exclude: ['/admin*'],
}),
ServeStaticModule.forRoot({
rootPath: join('/app', 'static', 'admin'),
serveRoot: '/admin',
})
);
.useIf(config => config.isSelfhosted, SelfhostModule);
// plugin modules
ENABLED_PLUGINS.forEach(name => {

View File

@@ -16,6 +16,7 @@ import {
EmailTokenNotFound,
EmailVerificationRequired,
InvalidEmailToken,
LinkExpired,
SameEmailProvided,
SkipThrottle,
Throttle,
@@ -89,12 +90,17 @@ export class AuthResolver {
};
}
@Mutation(() => UserType)
@Public()
@Mutation(() => Boolean)
async changePassword(
@CurrentUser() user: CurrentUser,
@Args('token') token: string,
@Args('newPassword') newPassword: string
@Args('newPassword') newPassword: string,
@Args('userId', { type: () => String, nullable: true }) userId?: string
) {
if (!userId) {
throw new LinkExpired();
}
const config = await this.config.runtime.fetchAll({
'auth/password.max': true,
'auth/password.min': true,
@@ -108,7 +114,7 @@ export class AuthResolver {
TokenType.ChangePassword,
token,
{
credential: user.id,
credential: userId,
}
);
@@ -116,10 +122,10 @@ export class AuthResolver {
throw new InvalidEmailToken();
}
await this.auth.changePassword(user.id, newPassword);
await this.auth.revokeUserSessions(user.id);
await this.auth.changePassword(userId, newPassword);
await this.auth.revokeUserSessions(userId);
return user;
return true;
}
@Mutation(() => UserType)
@@ -163,7 +169,7 @@ export class AuthResolver {
user.id
);
const url = this.url.link(callbackUrl, { token });
const url = this.url.link(callbackUrl, { userId: user.id, token });
const res = await this.auth.sendChangePasswordEmail(user.email, url);
@@ -176,19 +182,7 @@ export class AuthResolver {
@Args('callbackUrl') callbackUrl: string,
@Args('email', { nullable: true }) _email?: string
) {
if (!user.emailVerified) {
throw new EmailVerificationRequired();
}
const token = await this.token.createToken(
TokenType.ChangePassword,
user.id
);
const url = this.url.link(callbackUrl, { token });
const res = await this.auth.sendSetPasswordEmail(user.email, url);
return !res.rejected.length;
return this.sendChangePasswordEmail(user, callbackUrl);
}
// The change email step is:
@@ -305,6 +299,7 @@ export class AuthResolver {
TokenType.ChangePassword,
userId
);
return this.url.link(callbackUrl, { token });
return this.url.link(callbackUrl, { userId, token });
}
}

View File

@@ -83,7 +83,7 @@ export class AuthService implements OnApplicationBootstrap {
await this.quota.switchUserQuota(devUser.id, QuotaType.ProPlanV1);
await this.feature.addAdmin(devUser.id);
await this.feature.addCopilot(devUser.id);
} catch (e) {
} catch {
// ignore
}
}

View File

@@ -4,17 +4,23 @@ import { Module } from '@nestjs/common';
import {
ServerConfigResolver,
ServerFeatureConfigResolver,
ServerRuntimeConfigResolver,
ServerServiceConfigResolver,
} from './resolver';
import { ServerService } from './service';
@Module({
providers: [
ServerService,
ServerConfigResolver,
ServerFeatureConfigResolver,
ServerRuntimeConfigResolver,
ServerServiceConfigResolver,
],
exports: [ServerService],
})
export class ServerConfigModule {}
export { ServerService };
export { ADD_ENABLED_FEATURES } from './server-feature';
export { ServerFeature } from './types';

View File

@@ -9,7 +9,7 @@ import {
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { PrismaClient, RuntimeConfig, RuntimeConfigType } from '@prisma/client';
import { RuntimeConfig, RuntimeConfigType } from '@prisma/client';
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
import { Config, URLHelper } from '../../fundamentals';
@@ -19,6 +19,7 @@ import { FeatureType } from '../features';
import { AvailableUserFeatureConfig } from '../features/resolver';
import { ServerFlags } from './config';
import { ENABLED_FEATURES } from './server-feature';
import { ServerService } from './service';
import { ServerConfigType } from './types';
@ObjectType()
@@ -76,7 +77,7 @@ export class ServerConfigResolver {
constructor(
private readonly config: Config,
private readonly url: URLHelper,
private readonly db: PrismaClient
private readonly server: ServerService
) {}
@Public()
@@ -131,7 +132,7 @@ export class ServerConfigResolver {
description: 'whether server has been initialized',
})
async initialized() {
return (await this.db.user.count()) > 0;
return this.server.initialized();
}
}

View File

@@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class ServerService {
private _initialized: boolean | null = null;
constructor(private readonly db: PrismaClient) {}
async initialized() {
if (!this._initialized) {
const userCount = await this.db.user.count();
this._initialized = userCount > 0;
}
return this._initialized;
}
}

View File

@@ -0,0 +1,179 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { Mutex } from '../../../fundamentals';
import { DocStorageOptions } from '../options';
import { DocRecord, DocStorageAdapter } from '../storage';
@Injectable()
export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
constructor(
private readonly db: PrismaClient,
private readonly mutex: Mutex,
options: DocStorageOptions
) {
super(options);
}
// no updates queue for userspace, directly merge them inplace
// no history record for userspace
protected async getDocUpdates() {
return [];
}
protected async markUpdatesMerged() {
return 0;
}
async listDocHistories() {
return [];
}
async getDocHistory() {
return null;
}
protected async createDocHistory() {
return false;
}
override async rollbackDoc() {
return;
}
override async getDoc(spaceId: string, docId: string) {
return this.getDocSnapshot(spaceId, docId);
}
async pushDocUpdates(userId: string, docId: string, updates: Uint8Array[]) {
if (!updates.length) {
return 0;
}
await using _lock = await this.lockDocForUpdate(userId, docId);
const snapshot = await this.getDocSnapshot(userId, docId);
const now = Date.now();
const pendings = updates.map((update, i) => ({
bin: update,
timestamp: now + i,
}));
const { timestamp, bin } = await this.squash(
snapshot ? [snapshot, ...pendings] : pendings
);
await this.setDocSnapshot({
spaceId: userId,
docId,
bin,
timestamp,
});
return timestamp;
}
async deleteDoc(userId: string, docId: string) {
await this.db.userSnapshot.deleteMany({
where: {
userId,
id: docId,
},
});
}
async deleteSpace(userId: string) {
await this.db.userSnapshot.deleteMany({
where: {
userId,
},
});
}
async getSpaceDocTimestamps(userId: string, after?: number) {
const snapshots = await this.db.userSnapshot.findMany({
select: {
id: true,
updatedAt: true,
},
where: {
userId,
...(after
? {
updatedAt: {
gt: new Date(after),
},
}
: {}),
},
});
const result: Record<string, number> = {};
snapshots.forEach(s => {
result[s.id] = s.updatedAt.getTime();
});
return result;
}
protected async getDocSnapshot(userId: string, docId: string) {
const snapshot = await this.db.userSnapshot.findUnique({
where: {
userId_id: {
userId,
id: docId,
},
},
});
if (!snapshot) {
return null;
}
return {
spaceId: userId,
docId,
bin: snapshot.blob,
timestamp: snapshot.updatedAt.getTime(),
};
}
protected async setDocSnapshot(snapshot: DocRecord) {
// we always get lock before writing to user snapshot table,
// so a simple upsert without testing on updatedAt is safe
await this.db.userSnapshot.upsert({
where: {
userId_id: {
userId: snapshot.spaceId,
id: snapshot.docId,
},
},
update: {
blob: Buffer.from(snapshot.bin),
updatedAt: new Date(snapshot.timestamp),
},
create: {
userId: snapshot.spaceId,
id: snapshot.docId,
blob: Buffer.from(snapshot.bin),
createdAt: new Date(snapshot.timestamp),
updatedAt: new Date(snapshot.timestamp),
},
});
return true;
}
protected override async lockDocForUpdate(
workspaceId: string,
docId: string
) {
const lock = await this.mutex.lock(`userspace:${workspaceId}:${docId}`);
if (!lock) {
throw new Error('Too many concurrent writings');
}
return lock;
}
}

View File

@@ -0,0 +1,594 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { chunk } from 'lodash-es';
import {
Cache,
DocHistoryNotFound,
DocNotFound,
FailedToSaveUpdates,
FailedToUpsertSnapshot,
metrics,
Mutex,
} from '../../../fundamentals';
import { retryable } from '../../../fundamentals/utils/promise';
import { DocStorageOptions } from '../options';
import {
DocRecord,
DocStorageAdapter,
DocUpdate,
HistoryFilter,
} from '../storage';
const UPDATES_QUEUE_CACHE_KEY = 'doc:manager:updates';
@Injectable()
export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
private readonly logger = new Logger(PgWorkspaceDocStorageAdapter.name);
constructor(
private readonly db: PrismaClient,
private readonly mutex: Mutex,
private readonly cache: Cache,
protected override readonly options: DocStorageOptions
) {
super(options);
}
async pushDocUpdates(
workspaceId: string,
docId: string,
updates: Uint8Array[]
) {
if (!updates.length) {
return 0;
}
let pendings = updates;
let done = 0;
let timestamp = Date.now();
try {
await retryable(async () => {
if (done !== 0) {
pendings = pendings.slice(done);
}
// TODO(@forehalo): remove in next release
const lastSeq = await this.getUpdateSeq(
workspaceId,
docId,
updates.length
);
let turn = 0;
const batchCount = 10;
for (const batch of chunk(pendings, batchCount)) {
const now = Date.now();
await this.db.update.createMany({
data: batch.map((update, i) => {
const subSeq = turn * batchCount + i + 1;
// `seq` is the last seq num of the batch
// example for 11 batched updates, start from seq num 20
// seq for first update in the batch should be:
// 31 - 11 + subSeq(0 * 10 + 0 + 1) = 21
// ^ last seq num ^ updates.length ^ turn ^ batchCount ^i
const seq = lastSeq - updates.length + subSeq;
const createdAt = now + subSeq;
timestamp = Math.max(timestamp, createdAt);
return {
workspaceId,
id: docId,
blob: Buffer.from(update),
seq,
createdAt: new Date(createdAt),
};
}),
});
turn++;
done += batch.length;
await this.updateCachedUpdatesCount(workspaceId, docId, batch.length);
}
});
} catch (e) {
this.logger.error('Failed to insert doc updates', e);
metrics.doc.counter('doc_update_insert_failed').add(1);
throw new FailedToSaveUpdates();
}
return timestamp;
}
protected async getDocUpdates(workspaceId: string, docId: string) {
const rows = await this.db.update.findMany({
where: {
workspaceId,
id: docId,
},
orderBy: {
createdAt: 'asc',
},
});
return rows.map(row => ({
bin: row.blob,
timestamp: row.createdAt.getTime(),
}));
}
async deleteDoc(workspaceId: string, docId: string) {
const ident = { where: { workspaceId, id: docId } };
await this.db.$transaction([
this.db.snapshot.deleteMany(ident),
this.db.update.deleteMany(ident),
this.db.snapshotHistory.deleteMany(ident),
]);
}
async deleteSpace(workspaceId: string) {
const ident = { where: { workspaceId } };
await this.db.$transaction([
this.db.workspace.deleteMany({
where: {
id: workspaceId,
},
}),
this.db.snapshot.deleteMany(ident),
this.db.update.deleteMany(ident),
this.db.snapshotHistory.deleteMany(ident),
]);
}
async getSpaceDocTimestamps(workspaceId: string, after?: number) {
const snapshots = await this.db.snapshot.findMany({
select: {
id: true,
updatedAt: true,
},
where: {
workspaceId,
...(after
? {
updatedAt: {
gt: new Date(after),
},
}
: {}),
},
});
const updates = await this.db.update.groupBy({
where: {
workspaceId,
...(after
? {
createdAt: {
gt: new Date(after),
},
}
: {}),
},
by: ['id'],
_max: {
createdAt: true,
},
});
const result: Record<string, number> = {};
snapshots.forEach(s => {
result[s.id] = s.updatedAt.getTime();
});
updates.forEach(u => {
if (u._max.createdAt) {
result[u.id] = u._max.createdAt.getTime();
}
});
return result;
}
protected async markUpdatesMerged(
workspaceId: string,
docId: string,
updates: DocUpdate[]
) {
const result = await this.db.update.deleteMany({
where: {
workspaceId,
id: docId,
createdAt: {
in: updates.map(u => new Date(u.timestamp)),
},
},
});
await this.updateCachedUpdatesCount(workspaceId, docId, -result.count);
return result.count;
}
async listDocHistories(
workspaceId: string,
docId: string,
query: HistoryFilter
) {
const histories = await this.db.snapshotHistory.findMany({
select: {
timestamp: true,
},
where: {
workspaceId,
id: docId,
timestamp: {
lt: query.before ? new Date(query.before) : new Date(),
},
},
orderBy: {
timestamp: 'desc',
},
take: query.limit,
});
return histories.map(h => h.timestamp.getTime());
}
async getDocHistory(workspaceId: string, docId: string, timestamp: number) {
const history = await this.db.snapshotHistory.findUnique({
where: {
workspaceId_id_timestamp: {
workspaceId,
id: docId,
timestamp: new Date(timestamp),
},
},
});
if (!history) {
return null;
}
return {
spaceId: workspaceId,
docId,
bin: history.blob,
timestamp,
};
}
override async rollbackDoc(
spaceId: string,
docId: string,
timestamp: number
): Promise<void> {
await using _lock = await this.lockDocForUpdate(spaceId, docId);
const toSnapshot = await this.getDocHistory(spaceId, docId, timestamp);
if (!toSnapshot) {
throw new DocHistoryNotFound({ spaceId, docId, timestamp });
}
const fromSnapshot = await this.getDocSnapshot(spaceId, docId);
if (!fromSnapshot) {
throw new DocNotFound({ spaceId, docId });
}
// force create a new history record after rollback
await this.createDocHistory(fromSnapshot, true);
// WARN:
// we should never do the snapshot updating in recovering,
// which is not the solution in CRDT.
// let user revert in client and update the data in sync system
// const change = this.generateChangeUpdate(fromSnapshot.bin, toSnapshot.bin);
// await this.pushDocUpdates(spaceId, docId, [change]);
metrics.doc
.counter('history_recovered_counter', {
description: 'How many times history recovered request happened',
})
.add(1);
}
protected async createDocHistory(snapshot: DocRecord, force = false) {
const last = await this.lastDocHistory(snapshot.spaceId, snapshot.docId);
let shouldCreateHistory = false;
if (!last) {
// never created
shouldCreateHistory = true;
} else {
const lastHistoryTimestamp = last.timestamp.getTime();
if (lastHistoryTimestamp === snapshot.timestamp) {
// no change
shouldCreateHistory = false;
} else if (
// force
force ||
// last history created before interval in configs
lastHistoryTimestamp <
snapshot.timestamp - this.options.historyMinInterval(snapshot.spaceId)
) {
shouldCreateHistory = true;
}
}
if (shouldCreateHistory) {
if (this.isEmptyBin(snapshot.bin)) {
this.logger.debug(
`Doc is empty, skip creating history record for ${snapshot.docId} in workspace ${snapshot.spaceId}`
);
return false;
}
await this.db.snapshotHistory
.create({
select: {
timestamp: true,
},
data: {
workspaceId: snapshot.spaceId,
id: snapshot.docId,
timestamp: new Date(snapshot.timestamp),
blob: Buffer.from(snapshot.bin),
expiredAt: new Date(
Date.now() + (await this.options.historyMaxAge(snapshot.spaceId))
),
},
})
.catch(() => {
// safe to ignore
// only happens when duplicated history record created in multi processes
});
metrics.doc
.counter('history_created_counter', {
description: 'How many times the snapshot history created',
})
.add(1);
this.logger.debug(
`History created for ${snapshot.docId} in workspace ${snapshot.spaceId}.`
);
return true;
}
return false;
}
protected async getDocSnapshot(workspaceId: string, docId: string) {
const snapshot = await this.db.snapshot.findUnique({
where: {
id_workspaceId: {
workspaceId,
id: docId,
},
},
});
if (!snapshot) {
return null;
}
return {
spaceId: workspaceId,
docId,
bin: snapshot.blob,
timestamp: snapshot.updatedAt.getTime(),
};
}
protected async setDocSnapshot(snapshot: DocRecord) {
const { spaceId, docId, bin, timestamp } = snapshot;
if (this.isEmptyBin(bin)) {
return false;
}
const updatedAt = new Date(timestamp);
// CONCERNS:
// i. Because we save the real user's last seen action time as `updatedAt`,
// it's possible to simply compare the `updatedAt` to determine if the snapshot is older than the one we are going to save.
//
// ii. Prisma doesn't support `upsert` with additional `where` condition along side unique constraint.
// In our case, we need to manually check the `updatedAt` to avoid overriding the newer snapshot.
// where: { id_workspaceId: {}, updatedAt: { lt: updatedAt } }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
try {
const result: { updatedAt: Date }[] = await this.db.$queryRaw`
INSERT INTO "snapshots" ("workspace_id", "guid", "blob", "created_at", "updated_at")
VALUES (${spaceId}, ${docId}, ${bin}, DEFAULT, ${updatedAt})
ON CONFLICT ("workspace_id", "guid")
DO UPDATE SET "blob" = ${bin}, "updated_at" = ${updatedAt}
WHERE "snapshots"."workspace_id" = ${spaceId} AND "snapshots"."guid" = ${docId} AND "snapshots"."updated_at" <= ${updatedAt}
RETURNING "snapshots"."workspace_id" as "workspaceId", "snapshots"."guid" as "id", "snapshots"."updated_at" as "updatedAt"
`;
// const result = await this.db.snapshot.upsert({
// select: {
// updatedAt: true,
// seq: true,
// },
// where: {
// id_workspaceId: {
// workspaceId,
// id: guid,
// },
// ⬇️ NOT SUPPORTED BY PRISMA YET
// updatedAt: {
// lt: updatedAt,
// },
// },
// update: {
// blob,
// state,
// updatedAt,
// },
// create: {
// workspaceId,
// id: guid,
// blob,
// state,
// updatedAt,
// seq,
// },
// });
// if the condition `snapshot.updatedAt > updatedAt` is true, by which means the snapshot has already been updated by other process,
// the updates has been applied to current `doc` must have been seen by the other process as well.
// The `updatedSnapshot` will be `undefined` in this case.
const updatedSnapshot = result.at(0);
return !!updatedSnapshot;
} catch (e) {
metrics.doc.counter('snapshot_upsert_failed').add(1);
this.logger.error('Failed to upsert snapshot', e);
throw new FailedToUpsertSnapshot();
}
}
protected override async lockDocForUpdate(
workspaceId: string,
docId: string
) {
const lock = await this.mutex.lock(`doc:update:${workspaceId}:${docId}`);
if (!lock) {
throw new Error('Too many concurrent writings');
}
return lock;
}
protected async lastDocHistory(workspaceId: string, id: string) {
return this.db.snapshotHistory.findFirst({
where: {
workspaceId,
id,
},
select: {
timestamp: true,
state: true,
},
orderBy: {
timestamp: 'desc',
},
});
}
// for auto merging
async randomDoc() {
const key = await this.cache.mapRandomKey(UPDATES_QUEUE_CACHE_KEY);
if (key) {
const cachedCount = await this.cache.mapIncrease(
UPDATES_QUEUE_CACHE_KEY,
key,
0
);
if (cachedCount > 0) {
const [workspaceId, id] = key.split('::');
const count = await this.db.update.count({
where: {
workspaceId,
id,
},
});
// FIXME(@forehalo): somehow the update count in cache is not accurate
if (count === 0) {
metrics.doc
.counter('doc_update_count_inconsistent_with_cache')
.add(1);
await this.cache.mapDelete(UPDATES_QUEUE_CACHE_KEY, key);
return null;
}
return { workspaceId, docId: id };
}
}
return null;
}
private async updateCachedUpdatesCount(
workspaceId: string,
guid: string,
count: number
) {
const result = await this.cache.mapIncrease(
UPDATES_QUEUE_CACHE_KEY,
`${workspaceId}::${guid}`,
count
);
if (result <= 0) {
await this.cache.mapDelete(
UPDATES_QUEUE_CACHE_KEY,
`${workspaceId}::${guid}`
);
}
}
/**
* @deprecated
*/
private readonly seqMap = new Map<string, number>();
/**
*
* @deprecated updates do not rely on seq number anymore
*
* keep in next release to avoid downtime when upgrading instances
*/
private async getUpdateSeq(workspaceId: string, guid: string, batch = 1) {
const MAX_SEQ_NUM = 0x3fffffff; // u31
try {
const { seq } = await this.db.snapshot.update({
select: {
seq: true,
},
where: {
id_workspaceId: {
workspaceId,
id: guid,
},
},
data: {
seq: {
increment: batch,
},
},
});
if (!seq) {
return batch;
}
// reset
if (seq >= MAX_SEQ_NUM) {
await this.db.snapshot.update({
select: {
seq: true,
},
where: {
id_workspaceId: {
workspaceId,
id: guid,
},
},
data: {
seq: 0,
},
});
}
return seq;
} catch {
// not existing snapshot just count it from 1
const last = this.seqMap.get(workspaceId + guid) ?? 0;
this.seqMap.set(workspaceId + guid, last + batch);
return last + batch;
}
}
}

View File

@@ -1,266 +0,0 @@
import { isDeepStrictEqual } from 'node:util';
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaClient } from '@prisma/client';
import type { EventPayload } from '../../fundamentals';
import {
Config,
DocHistoryNotFound,
DocNotFound,
metrics,
OnEvent,
WorkspaceNotFound,
} from '../../fundamentals';
import { QuotaService } from '../quota';
import { Permission } from '../workspaces/types';
import { isEmptyBuffer } from './manager';
@Injectable()
export class DocHistoryManager {
private readonly logger = new Logger(DocHistoryManager.name);
constructor(
private readonly config: Config,
private readonly db: PrismaClient,
private readonly quota: QuotaService
) {}
@OnEvent('workspace.deleted')
onWorkspaceDeleted(workspaceId: EventPayload<'workspace.deleted'>) {
return this.db.snapshotHistory.deleteMany({
where: {
workspaceId,
},
});
}
@OnEvent('snapshot.deleted')
onSnapshotDeleted({ workspaceId, id }: EventPayload<'snapshot.deleted'>) {
return this.db.snapshotHistory.deleteMany({
where: {
workspaceId,
id,
},
});
}
@OnEvent('snapshot.updated')
async onDocUpdated(
{ workspaceId, id, previous }: EventPayload<'snapshot.updated'>,
forceCreate = false
) {
const last = await this.last(workspaceId, id);
let shouldCreateHistory = false;
if (!last) {
// never created
shouldCreateHistory = true;
} else if (last.timestamp === previous.updatedAt) {
// no change
shouldCreateHistory = false;
} else if (
// force
forceCreate ||
// last history created before interval in configs
last.timestamp.getTime() <
previous.updatedAt.getTime() - this.config.doc.history.interval
) {
shouldCreateHistory = true;
}
if (shouldCreateHistory) {
// skip the history recording when no actual update on snapshot happended
if (last && isDeepStrictEqual(last.state, previous.state)) {
this.logger.debug(
`State matches, skip creating history record for ${id} in workspace ${workspaceId}`
);
return;
}
if (isEmptyBuffer(previous.blob)) {
this.logger.debug(
`Doc is empty, skip creating history record for ${id} in workspace ${workspaceId}`
);
return;
}
await this.db.snapshotHistory
.create({
select: {
timestamp: true,
},
data: {
workspaceId,
id,
timestamp: previous.updatedAt,
blob: previous.blob,
state: previous.state,
expiredAt: await this.getExpiredDateFromNow(workspaceId),
},
})
.catch(() => {
// safe to ignore
// only happens when duplicated history record created in multi processes
});
metrics.doc
.counter('history_created_counter', {
description: 'How many times the snapshot history created',
})
.add(1);
this.logger.debug(
`History created for ${id} in workspace ${workspaceId}.`
);
}
}
async list(
workspaceId: string,
id: string,
before: Date = new Date(),
take: number = 10
) {
return this.db.snapshotHistory.findMany({
select: {
timestamp: true,
},
where: {
workspaceId,
id,
timestamp: {
lt: before,
},
// only include the ones has not expired
expiredAt: {
gt: new Date(),
},
},
orderBy: {
timestamp: 'desc',
},
take,
});
}
async count(workspaceId: string, id: string) {
return this.db.snapshotHistory.count({
where: {
workspaceId,
id,
expiredAt: {
gt: new Date(),
},
},
});
}
async get(workspaceId: string, id: string, timestamp: Date) {
return this.db.snapshotHistory.findUnique({
where: {
workspaceId_id_timestamp: {
workspaceId,
id,
timestamp,
},
expiredAt: {
gt: new Date(),
},
},
});
}
async last(workspaceId: string, id: string) {
return this.db.snapshotHistory.findFirst({
where: {
workspaceId,
id,
},
select: {
timestamp: true,
state: true,
},
orderBy: {
timestamp: 'desc',
},
});
}
async recover(workspaceId: string, id: string, timestamp: Date) {
const history = await this.db.snapshotHistory.findUnique({
where: {
workspaceId_id_timestamp: {
workspaceId,
id,
timestamp,
},
},
});
if (!history) {
throw new DocHistoryNotFound({
workspaceId,
docId: id,
timestamp: timestamp.getTime(),
});
}
const oldSnapshot = await this.db.snapshot.findUnique({
where: {
id_workspaceId: {
id,
workspaceId,
},
},
});
if (!oldSnapshot) {
throw new DocNotFound({ workspaceId, docId: id });
}
// save old snapshot as one history record
await this.onDocUpdated({ workspaceId, id, previous: oldSnapshot }, true);
// WARN:
// we should never do the snapshot updating in recovering,
// which is not the solution in CRDT.
// let user revert in client and update the data in sync system
// `await this.db.snapshot.update();`
metrics.doc
.counter('history_recovered_counter', {
description: 'How many times history recovered request happened',
})
.add(1);
return history.timestamp;
}
async getExpiredDateFromNow(workspaceId: string) {
const permission = await this.db.workspaceUserPermission.findFirst({
select: {
userId: true,
},
where: {
workspaceId,
type: Permission.Owner,
},
});
if (!permission) {
throw new WorkspaceNotFound({ workspaceId });
}
const quota = await this.quota.getUserQuota(permission.userId);
return quota.feature.historyPeriodFromNow;
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT /* everyday at 12am */)
async cleanupExpiredHistory() {
await this.db.snapshotHistory.deleteMany({
where: {
expiredAt: {
lte: new Date(),
},
},
});
}
}

View File

@@ -2,15 +2,24 @@ import './config';
import { Module } from '@nestjs/common';
import { PermissionModule } from '../permission';
import { QuotaModule } from '../quota';
import { DocHistoryManager } from './history';
import { DocManager } from './manager';
import { PgUserspaceDocStorageAdapter } from './adapters/userspace';
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
import { DocStorageCronJob } from './job';
import { DocStorageOptions } from './options';
@Module({
imports: [QuotaModule],
providers: [DocManager, DocHistoryManager],
exports: [DocManager, DocHistoryManager],
imports: [QuotaModule, PermissionModule],
providers: [
DocStorageOptions,
PgWorkspaceDocStorageAdapter,
PgUserspaceDocStorageAdapter,
DocStorageCronJob,
],
exports: [PgWorkspaceDocStorageAdapter, PgUserspaceDocStorageAdapter],
})
export class DocModule {}
export class DocStorageModule {}
export { PgUserspaceDocStorageAdapter, PgWorkspaceDocStorageAdapter };
export { DocHistoryManager, DocManager };
export { DocStorageAdapter } from './storage';

View File

@@ -0,0 +1,76 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
import { PrismaClient } from '@prisma/client';
import { CallTimer, Config, metrics } from '../../fundamentals';
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
@Injectable()
export class DocStorageCronJob implements OnModuleInit {
private readonly logger = new Logger(DocStorageCronJob.name);
private busy = false;
constructor(
private readonly registry: SchedulerRegistry,
private readonly config: Config,
private readonly db: PrismaClient,
private readonly workspace: PgWorkspaceDocStorageAdapter
) {}
onModuleInit() {
if (this.config.doc.manager.enableUpdateAutoMerging) {
this.registry.addInterval(
this.autoMergePendingDocUpdates.name,
// scheduler registry will clean up the interval when the app is stopped
setInterval(() => {
if (this.busy) {
return;
}
this.busy = true;
this.autoMergePendingDocUpdates()
.catch(() => {
/* never fail */
})
.finally(() => {
this.busy = false;
});
}, this.config.doc.manager.updatePollInterval)
);
this.logger.log('Updates pending queue auto merging cron started');
}
}
@CallTimer('doc', 'auto_merge_pending_doc_updates')
async autoMergePendingDocUpdates() {
try {
const randomDoc = await this.workspace.randomDoc();
if (!randomDoc) {
return;
}
await this.workspace.getDoc(randomDoc.workspaceId, randomDoc.docId);
} catch (e) {
metrics.doc.counter('auto_merge_pending_doc_updates_error').add(1);
this.logger.error('Failed to auto merge pending doc updates', e);
}
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT /* everyday at 12am */)
async cleanupExpiredHistory() {
await this.db.snapshotHistory.deleteMany({
where: {
expiredAt: {
lte: new Date(),
},
},
});
}
@Cron(CronExpression.EVERY_MINUTE)
async reportUpdatesQueueCount() {
metrics.doc
.gauge('updates_queue_count')
.record(await this.db.update.count());
}
}

View File

@@ -1,853 +0,0 @@
import {
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaClient, Snapshot, Update } from '@prisma/client';
import { chunk } from 'lodash-es';
import { defer, retry } from 'rxjs';
import {
applyUpdate,
Doc,
encodeStateAsUpdate,
encodeStateVector,
transact,
} from 'yjs';
import type { EventPayload } from '../../fundamentals';
import {
Cache,
CallTimer,
Config,
EventEmitter,
mergeUpdatesInApplyWay as jwstMergeUpdates,
metrics,
OnEvent,
} from '../../fundamentals';
function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean {
if (yBinary.equals(jwstBinary)) {
return true;
}
if (strict) {
return false;
}
const doc = new Doc();
applyUpdate(doc, jwstBinary);
const yBinary2 = Buffer.from(encodeStateAsUpdate(doc));
return compare(yBinary, yBinary2, true);
}
export function isEmptyBuffer(buf: Buffer): boolean {
return (
buf.length === 0 ||
// 0x0000
(buf.length === 2 && buf[0] === 0 && buf[1] === 0)
);
}
const MAX_SEQ_NUM = 0x3fffffff; // u31
const UPDATES_QUEUE_CACHE_KEY = 'doc:manager:updates';
interface DocResponse {
doc: Doc;
timestamp: number;
}
interface BinaryResponse {
binary: Buffer;
timestamp: number;
}
/**
* Since we can't directly save all client updates into database, in which way the database will overload,
* we need to buffer the updates and merge them to reduce db write.
*
* And also, if a new client join, it would be nice to see the latest doc asap,
* so we need to at least store a snapshot of the doc and return quickly,
* along side all the updates that have not been applies to that snapshot(timestamp).
*/
@Injectable()
export class DocManager implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(DocManager.name);
private job: NodeJS.Timeout | null = null;
private readonly seqMap = new Map<string, number>();
private busy = false;
constructor(
private readonly db: PrismaClient,
private readonly config: Config,
private readonly cache: Cache,
private readonly event: EventEmitter
) {}
onModuleInit() {
if (this.config.doc.manager.enableUpdateAutoMerging) {
this.logger.log('Use Database');
this.setup();
}
}
onModuleDestroy() {
this.destroy();
}
@CallTimer('doc', 'yjs_recover_updates_to_doc')
private recoverDoc(...updates: Buffer[]): Promise<Doc> {
const doc = new Doc();
const chunks = chunk(updates, 10);
return new Promise(resolve => {
const next = () => {
const updates = chunks.shift();
if (updates?.length) {
transact(doc, () => {
updates.forEach(u => {
try {
applyUpdate(doc, u);
} catch (e) {
this.logger.error('Failed to apply update', e);
}
});
});
// avoid applying too many updates in single round which will take the whole cpu time like dead lock
setImmediate(() => {
next();
});
} else {
resolve(doc);
}
};
next();
});
}
private async applyUpdates(guid: string, ...updates: Buffer[]): Promise<Doc> {
const doc = await this.recoverDoc(...updates);
const useYocto = await this.config.runtime.fetch(
'doc/experimentalMergeWithYOcto'
);
// test jwst codec
if (useYocto) {
metrics.jwst.counter('codec_merge_counter').add(1);
const yjsResult = Buffer.from(encodeStateAsUpdate(doc));
let log = false;
try {
const jwstResult = jwstMergeUpdates(updates);
if (!compare(yjsResult, jwstResult)) {
metrics.jwst.counter('codec_not_match').add(1);
this.logger.warn(
`jwst codec result doesn't match yjs codec result for: ${guid}`
);
log = true;
if (this.config.node.dev) {
this.logger.warn(`Expected:\n ${yjsResult.toString('hex')}`);
this.logger.warn(`Result:\n ${jwstResult.toString('hex')}`);
}
}
} catch (e) {
metrics.jwst.counter('codec_fails_counter').add(1);
this.logger.warn(`jwst apply update failed for ${guid}: ${e}`);
log = true;
} finally {
if (log && this.config.node.dev) {
this.logger.warn(
`Updates: ${updates.map(u => u.toString('hex')).join('\n')}`
);
}
}
}
return doc;
}
/**
* setup pending update processing loop
*/
setup() {
this.job = setInterval(() => {
if (!this.busy) {
this.busy = true;
this.autoSquash()
.catch(() => {
/* we handle all errors in work itself */
})
.finally(() => {
this.busy = false;
});
}
}, this.config.doc.manager.updatePollInterval);
this.logger.log('Automation started');
}
/**
* stop pending update processing loop
*/
destroy() {
if (this.job) {
clearInterval(this.job);
this.job = null;
this.logger.log('Automation stopped');
}
}
@OnEvent('workspace.deleted')
async onWorkspaceDeleted(workspaceId: string) {
await this.db.snapshot.deleteMany({
where: {
workspaceId,
},
});
await this.db.update.deleteMany({
where: {
workspaceId,
},
});
}
@OnEvent('snapshot.deleted')
async onSnapshotDeleted({
id,
workspaceId,
}: EventPayload<'snapshot.deleted'>) {
await this.db.update.deleteMany({
where: {
id,
workspaceId,
},
});
}
/**
* add update to manager for later processing.
*/
async push(
workspaceId: string,
guid: string,
update: Buffer,
retryTimes = 10
) {
const timestamp = await new Promise<number>((resolve, reject) => {
defer(async () => {
const seq = await this.getUpdateSeq(workspaceId, guid);
const { createdAt } = await this.db.update.create({
select: {
createdAt: true,
},
data: {
workspaceId,
id: guid,
seq,
blob: update,
},
});
return createdAt.getTime();
})
.pipe(retry(retryTimes)) // retry until seq num not conflict
.subscribe({
next: timestamp => {
this.logger.debug(
`pushed 1 update for ${guid} in workspace ${workspaceId}`
);
resolve(timestamp);
},
error: e => {
this.logger.error('Failed to push updates', e);
reject(new Error('Failed to push update'));
},
});
});
await this.updateCachedUpdatesCount(workspaceId, guid, 1);
return timestamp;
}
async batchPush(
workspaceId: string,
guid: string,
updates: Buffer[],
retryTimes = 10
) {
const timestamp = await new Promise<number>((resolve, reject) => {
defer(async () => {
const lastSeq = await this.getUpdateSeq(
workspaceId,
guid,
updates.length
);
const now = Date.now();
let timestamp = now;
let turn = 0;
const batchCount = 10;
for (const batch of chunk(updates, batchCount)) {
await this.db.update.createMany({
data: batch.map((update, i) => {
const subSeq = turn * batchCount + i + 1;
// `seq` is the last seq num of the batch
// example for 11 batched updates, start from seq num 20
// seq for first update in the batch should be:
// 31 - 11 + subSeq(0 * 10 + 0 + 1) = 21
// ^ last seq num ^ updates.length ^ turn ^ batchCount ^i
const seq = lastSeq - updates.length + subSeq;
const createdAt = now + subSeq;
timestamp = Math.max(timestamp, createdAt);
return {
workspaceId,
id: guid,
blob: update,
seq,
createdAt: new Date(createdAt), // make sure the updates can be ordered by create time
};
}),
});
turn++;
}
return timestamp;
})
.pipe(retry(retryTimes)) // retry until seq num not conflict
.subscribe({
next: timestamp => {
this.logger.debug(
`pushed ${updates.length} updates for ${guid} in workspace ${workspaceId}`
);
resolve(timestamp);
},
error: e => {
this.logger.error('Failed to push updates', e);
reject(new Error('Failed to push update'));
},
});
});
await this.updateCachedUpdatesCount(workspaceId, guid, updates.length);
return timestamp;
}
/**
* Get latest timestamp of all docs in the workspace.
*/
@CallTimer('doc', 'get_doc_timestamps')
async getDocTimestamps(workspaceId: string, after: number | undefined = 0) {
const snapshots = await this.db.snapshot.findMany({
where: {
workspaceId,
updatedAt: {
gt: new Date(after),
},
},
select: {
id: true,
updatedAt: true,
},
});
const updates = await this.db.update.groupBy({
where: {
workspaceId,
createdAt: {
gt: new Date(after),
},
},
by: ['id'],
_max: {
createdAt: true,
},
});
const result: Record<string, number> = {};
snapshots.forEach(s => {
result[s.id] = s.updatedAt.getTime();
});
updates.forEach(u => {
if (u._max.createdAt) {
result[u.id] = u._max.createdAt.getTime();
}
});
return result;
}
/**
* get the latest doc with all update applied.
*/
async get(workspaceId: string, guid: string): Promise<DocResponse | null> {
const result = await this._get(workspaceId, guid);
if (result) {
if ('doc' in result) {
return result;
} else {
const doc = await this.recoverDoc(result.binary);
return {
doc,
timestamp: result.timestamp,
};
}
}
return null;
}
/**
* get the latest doc binary with all update applied.
*/
async getBinary(
workspaceId: string,
guid: string
): Promise<BinaryResponse | null> {
const result = await this._get(workspaceId, guid);
if (result) {
if ('doc' in result) {
return {
binary: Buffer.from(encodeStateAsUpdate(result.doc)),
timestamp: result.timestamp,
};
} else {
return result;
}
}
return null;
}
/**
* get the latest doc state vector with all update applied.
*/
async getDocState(
workspaceId: string,
guid: string
): Promise<BinaryResponse | null> {
const snapshot = await this.getSnapshot(workspaceId, guid);
const updates = await this.getUpdates(workspaceId, guid);
if (updates.length) {
const { doc, timestamp } = await this.squash(snapshot, updates);
return {
binary: Buffer.from(encodeStateVector(doc)),
timestamp,
};
}
return snapshot?.state
? {
binary: snapshot.state,
timestamp: snapshot.updatedAt.getTime(),
}
: null;
}
/**
* get the snapshot of the doc we've seen.
*/
async getSnapshot(workspaceId: string, guid: string) {
return this.db.snapshot.findUnique({
where: {
id_workspaceId: {
workspaceId,
id: guid,
},
},
});
}
/**
* get pending updates
*/
async getUpdates(workspaceId: string, guid: string) {
const updates = await this.db.update.findMany({
where: {
workspaceId,
id: guid,
},
// take it ease, we don't want to overload db and or cpu
// if we limit the taken number here,
// user will never see the latest doc if there are too many updates pending to be merged.
take: this.config.doc.manager.maxUpdatesPullCount,
});
// perf(memory): avoid sorting in db
return updates.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
}
/**
* apply pending updates to snapshot
*/
private async autoSquash() {
// find the first update and batch process updates with same id
const candidate = await this.getAutoSquashCandidate();
// no pending updates
if (!candidate) {
return;
}
const { id, workspaceId } = candidate;
await this.lockUpdatesForAutoSquash(workspaceId, id, async () => {
try {
await this._get(workspaceId, id);
} catch (e) {
this.logger.error(
`Failed to apply updates for workspace: ${workspaceId}, guid: ${id}`
);
this.logger.error(e);
}
});
}
private async getAutoSquashCandidate() {
const cache = await this.getAutoSquashCandidateFromCache();
if (cache) {
return cache;
}
return this.db.update.findFirst({
select: {
id: true,
workspaceId: true,
},
});
}
/**
* @returns whether the snapshot is updated to the latest, `undefined` means the doc to be upserted is outdated.
*/
@CallTimer('doc', 'upsert')
private async upsert(
workspaceId: string,
guid: string,
doc: Doc,
// we always delay the snapshot update to avoid db overload,
// so the value of auto updated `updatedAt` by db will never be accurate to user's real action time
updatedAt: Date,
seq: number
) {
const blob = Buffer.from(encodeStateAsUpdate(doc));
if (isEmptyBuffer(blob)) {
return undefined;
}
const state = Buffer.from(encodeStateVector(doc));
// CONCERNS:
// i. Because we save the real user's last seen action time as `updatedAt`,
// it's possible to simply compare the `updatedAt` to determine if the snapshot is older than the one we are going to save.
//
// ii. Prisma doesn't support `upsert` with additional `where` condition along side unique constraint.
// In our case, we need to manually check the `updatedAt` to avoid overriding the newer snapshot.
// where: { id_workspaceId: {}, updatedAt: { lt: updatedAt } }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//
// iii. Only set the seq number when creating the snapshot.
// For updating scenario, the seq number will be updated when updates pushed to db.
try {
const result: { updatedAt: Date }[] = await this.db.$queryRaw`
INSERT INTO "snapshots" ("workspace_id", "guid", "blob", "state", "seq", "created_at", "updated_at")
VALUES (${workspaceId}, ${guid}, ${blob}, ${state}, ${seq}, DEFAULT, ${updatedAt})
ON CONFLICT ("workspace_id", "guid")
DO UPDATE SET "blob" = ${blob}, "state" = ${state}, "updated_at" = ${updatedAt}, "seq" = ${seq}
WHERE "snapshots"."workspace_id" = ${workspaceId} AND "snapshots"."guid" = ${guid} AND "snapshots"."updated_at" <= ${updatedAt}
RETURNING "snapshots"."workspace_id" as "workspaceId", "snapshots"."guid" as "id", "snapshots"."updated_at" as "updatedAt"
`;
// const result = await this.db.snapshot.upsert({
// select: {
// updatedAt: true,
// seq: true,
// },
// where: {
// id_workspaceId: {
// workspaceId,
// id: guid,
// },
// ⬇️ NOT SUPPORTED BY PRISMA YET
// updatedAt: {
// lt: updatedAt,
// },
// },
// update: {
// blob,
// state,
// updatedAt,
// },
// create: {
// workspaceId,
// id: guid,
// blob,
// state,
// updatedAt,
// seq,
// },
// });
// if the condition `snapshot.updatedAt > updatedAt` is true, by which means the snapshot has already been updated by other process,
// the updates has been applied to current `doc` must have been seen by the other process as well.
// The `updatedSnapshot` will be `undefined` in this case.
const updatedSnapshot = result.at(0);
if (!updatedSnapshot) {
return undefined;
}
return true;
} catch (e) {
this.logger.error('Failed to upsert snapshot', e);
return false;
}
}
private async _get(
workspaceId: string,
guid: string
): Promise<DocResponse | BinaryResponse | null> {
const snapshot = await this.getSnapshot(workspaceId, guid);
const updates = await this.getUpdates(workspaceId, guid);
if (updates.length) {
return this.squash(snapshot, updates);
}
return snapshot
? { binary: snapshot.blob, timestamp: snapshot.updatedAt.getTime() }
: null;
}
/**
* Squash updates into a single update and save it as snapshot,
* and delete the updates records at the same time.
*/
@CallTimer('doc', 'squash')
private async squash(
snapshot: Snapshot | null,
updates: Update[]
): Promise<DocResponse> {
if (!updates.length) {
throw new Error('No updates to squash');
}
const last = updates[updates.length - 1];
const { id, workspaceId } = last;
const doc = await this.applyUpdates(
id,
snapshot ? snapshot.blob : Buffer.from([0, 0]),
...updates.map(u => u.blob)
);
const done = await this.upsert(
workspaceId,
id,
doc,
last.createdAt,
last.seq
);
if (done) {
if (snapshot) {
this.event.emit('snapshot.updated', {
id,
workspaceId,
previous: {
blob: snapshot.blob,
state: snapshot.state,
updatedAt: snapshot.updatedAt,
},
});
}
this.logger.debug(
`Squashed ${updates.length} updates for ${id} in workspace ${workspaceId}`
);
}
// we will keep the updates only if the upsert failed on unknown reason
// `done === undefined` means the updates is outdated(have already been merged by other process), safe to be deleted
// `done === true` means the upsert is successful, safe to be deleted
if (done !== false) {
// always delete updates
// the upsert will return false if the state is not newer, so we don't need to worry about it
const { count } = await this.db.update.deleteMany({
where: {
id,
workspaceId,
seq: {
in: updates.map(u => u.seq),
},
},
});
await this.updateCachedUpdatesCount(workspaceId, id, -count);
}
return { doc, timestamp: last.createdAt.getTime() };
}
private async getUpdateSeq(workspaceId: string, guid: string, batch = 1) {
try {
const { seq } = await this.db.snapshot.update({
select: {
seq: true,
},
where: {
id_workspaceId: {
workspaceId,
id: guid,
},
},
data: {
seq: {
increment: batch,
},
},
});
// reset
if (seq >= MAX_SEQ_NUM) {
await this.db.snapshot.update({
select: {
seq: true,
},
where: {
id_workspaceId: {
workspaceId,
id: guid,
},
},
data: {
seq: 0,
},
});
}
return seq;
} catch {
// not existing snapshot just count it from 1
const last = this.seqMap.get(workspaceId + guid) ?? 0;
this.seqMap.set(workspaceId + guid, last + batch);
return last + batch;
}
}
private async updateCachedUpdatesCount(
workspaceId: string,
guid: string,
count: number
) {
const result = await this.cache.mapIncrease(
UPDATES_QUEUE_CACHE_KEY,
`${workspaceId}::${guid}`,
count
);
if (result <= 0) {
await this.cache.mapDelete(
UPDATES_QUEUE_CACHE_KEY,
`${workspaceId}::${guid}`
);
}
}
private async getAutoSquashCandidateFromCache() {
const key = await this.cache.mapRandomKey(UPDATES_QUEUE_CACHE_KEY);
if (key) {
const cachedCount = await this.cache.mapIncrease(
UPDATES_QUEUE_CACHE_KEY,
key,
0
);
if (cachedCount > 0) {
const [workspaceId, id] = key.split('::');
const count = await this.db.update.count({
where: {
workspaceId,
id,
},
});
// FIXME(@forehalo): somehow the update count in cache is not accurate
if (count === 0) {
await this.cache.mapDelete(UPDATES_QUEUE_CACHE_KEY, key);
return null;
}
return { id, workspaceId };
}
}
return null;
}
private async doWithLock<T>(
lockScope: string,
lockResource: string,
job: () => Promise<T>
) {
const lock = `lock:${lockScope}:${lockResource}`;
const acquired = await this.cache.setnx(lock, 1, {
ttl: 60 * 1000,
});
metrics.doc.counter('lock').add(1, { scope: lockScope });
if (!acquired) {
metrics.doc.counter('lock_failed').add(1, { scope: lockScope });
return;
}
metrics.doc.counter('lock_required').add(1, { scope: lockScope });
try {
return await job();
} finally {
await this.cache
.delete(lock)
.then(() => {
metrics.doc.counter('lock_released').add(1, { scope: lockScope });
})
.catch(e => {
metrics.doc
.counter('lock_release_failed')
.add(1, { scope: lockScope });
// safe, the lock will be expired when ttl ends
this.logger.error(`Failed to release lock ${lock}`, e);
});
}
}
private async lockUpdatesForAutoSquash<T>(
workspaceId: string,
guid: string,
job: () => Promise<T>
) {
return this.doWithLock(
'doc:manager:updates',
`${workspaceId}::${guid}`,
job
);
}
@Cron(CronExpression.EVERY_MINUTE)
async reportUpdatesQueueCount() {
metrics.doc
.gauge('updates_queue_count')
.record(await this.db.update.count());
}
}

View File

@@ -0,0 +1,130 @@
import { Injectable, Logger } from '@nestjs/common';
import { chunk } from 'lodash-es';
import * as Y from 'yjs';
import {
CallTimer,
Config,
mergeUpdatesInApplyWay as yotcoMergeUpdates,
metrics,
} from '../../fundamentals';
import { PermissionService } from '../permission';
import { QuotaService } from '../quota';
import { DocStorageOptions as IDocStorageOptions } from './storage';
function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean {
if (yBinary.equals(jwstBinary)) {
return true;
}
if (strict) {
return false;
}
const doc = new Y.Doc();
Y.applyUpdate(doc, jwstBinary);
const yBinary2 = Buffer.from(Y.encodeStateAsUpdate(doc));
return compare(yBinary, yBinary2, true);
}
@Injectable()
export class DocStorageOptions implements IDocStorageOptions {
private readonly logger = new Logger('DocStorageOptions');
constructor(
private readonly config: Config,
private readonly permission: PermissionService,
private readonly quota: QuotaService
) {}
mergeUpdates = async (updates: Uint8Array[]) => {
const useYocto = await this.config.runtime.fetch(
'doc/experimentalMergeWithYOcto'
);
if (useYocto) {
const doc = await this.recoverDoc(updates);
metrics.jwst.counter('codec_merge_counter').add(1);
const yjsResult = Buffer.from(Y.encodeStateAsUpdate(doc));
let log = false;
try {
const yocto = yotcoMergeUpdates(updates.map(Buffer.from));
if (!compare(yjsResult, yocto)) {
metrics.jwst.counter('codec_not_match').add(1);
this.logger.warn(`yocto codec result doesn't match yjs codec result`);
log = true;
if (this.config.node.dev) {
this.logger.warn(`Expected:\n ${yjsResult.toString('hex')}`);
this.logger.warn(`Result:\n ${yocto.toString('hex')}`);
}
}
} catch (e) {
metrics.jwst.counter('codec_fails_counter').add(1);
this.logger.warn(`jwst apply update failed: ${e}`);
log = true;
}
if (log && this.config.node.dev) {
this.logger.warn(
`Updates: ${updates.map(u => Buffer.from(u).toString('hex')).join('\n')}`
);
}
return yjsResult;
} else {
return this.simpleMergeUpdates(updates);
}
};
historyMaxAge = async (spaceId: string) => {
const owner = await this.permission.getWorkspaceOwner(spaceId);
const quota = await this.quota.getUserQuota(owner.id);
return quota.feature.historyPeriod;
};
historyMinInterval = (_spaceId: string) => {
return this.config.doc.history.interval;
};
@CallTimer('doc', 'yjs_merge_updates')
private simpleMergeUpdates(updates: Uint8Array[]) {
return Y.mergeUpdates(updates);
}
@CallTimer('doc', 'yjs_recover_updates_to_doc')
private recoverDoc(updates: Uint8Array[]): Promise<Y.Doc> {
const doc = new Y.Doc();
const chunks = chunk(updates, 10);
let i = 0;
return new Promise(resolve => {
Y.transact(doc, () => {
const next = () => {
const updates = chunks.at(i++);
if (updates?.length) {
updates.forEach(u => {
try {
Y.applyUpdate(doc, u);
} catch (e) {
this.logger.error('Failed to apply update', e);
}
});
// avoid applying too many updates in single round which will take the whole cpu time like dead lock
setImmediate(() => {
next();
});
} else {
resolve(doc);
}
};
next();
});
});
}
}

View File

@@ -0,0 +1,16 @@
import { Connection } from './connection';
export interface BlobStorageOptions {}
export interface Blob {
key: string;
bin: Uint8Array;
mimeType: string;
}
export abstract class BlobStorageAdapter extends Connection {
abstract getBlob(spaceId: string, key: string): Promise<Blob | null>;
abstract setBlob(spaceId: string, blob: Blob): Promise<string>;
abstract deleteBlob(spaceId: string, key: string): Promise<boolean>;
abstract listBlobs(spaceId: string): Promise<Blob>;
}

View File

@@ -0,0 +1,11 @@
export class Connection {
protected connected: boolean = false;
connect(): Promise<void> {
this.connected = true;
return Promise.resolve();
}
disconnect(): Promise<void> {
this.connected = false;
return Promise.resolve();
}
}

View File

@@ -0,0 +1,205 @@
import {
applyUpdate,
Doc,
encodeStateAsUpdate,
encodeStateVector,
mergeUpdates,
UndoManager,
} from 'yjs';
import { CallTimer } from '../../../fundamentals';
import { Connection } from './connection';
import { SingletonLocker } from './lock';
export interface DocRecord {
spaceId: string;
docId: string;
bin: Uint8Array;
timestamp: number;
}
export interface DocUpdate {
bin: Uint8Array;
timestamp: number;
}
export interface HistoryFilter {
before?: number;
limit?: number;
}
export interface DocStorageOptions {
mergeUpdates?: (updates: Uint8Array[]) => Promise<Uint8Array> | Uint8Array;
}
export abstract class DocStorageAdapter extends Connection {
private readonly locker = new SingletonLocker();
constructor(
protected readonly options: DocStorageOptions = {
mergeUpdates,
}
) {
super();
}
// open apis
isEmptyBin(bin: Uint8Array): boolean {
return (
bin.length === 0 ||
// 0x0 for state vector
(bin.length === 1 && bin[0] === 0) ||
// 0x00 for update
(bin.length === 2 && bin[0] === 0 && bin[1] === 0)
);
}
async getDoc(spaceId: string, docId: string): Promise<DocRecord | null> {
await using _lock = await this.lockDocForUpdate(spaceId, docId);
const snapshot = await this.getDocSnapshot(spaceId, docId);
const updates = await this.getDocUpdates(spaceId, docId);
if (updates.length) {
const { timestamp, bin } = await this.squash(
snapshot ? [snapshot, ...updates] : updates
);
const newSnapshot = {
spaceId: spaceId,
docId,
bin,
timestamp,
};
const success = await this.setDocSnapshot(newSnapshot);
// if there is old snapshot, create a new history record
if (success && snapshot) {
await this.createDocHistory(snapshot);
}
// always mark updates as merged unless throws
await this.markUpdatesMerged(spaceId, docId, updates);
return newSnapshot;
}
return snapshot;
}
abstract pushDocUpdates(
spaceId: string,
docId: string,
updates: Uint8Array[]
): Promise<number>;
abstract deleteDoc(spaceId: string, docId: string): Promise<void>;
abstract deleteSpace(spaceId: string): Promise<void>;
async rollbackDoc(
spaceId: string,
docId: string,
timestamp: number
): Promise<void> {
await using _lock = await this.lockDocForUpdate(spaceId, docId);
const toSnapshot = await this.getDocHistory(spaceId, docId, timestamp);
if (!toSnapshot) {
throw new Error('Can not find the version to rollback to.');
}
const fromSnapshot = await this.getDocSnapshot(spaceId, docId);
if (!fromSnapshot) {
throw new Error('Can not find the current version of the doc.');
}
const change = this.generateChangeUpdate(fromSnapshot.bin, toSnapshot.bin);
await this.pushDocUpdates(spaceId, docId, [change]);
// force create a new history record after rollback
await this.createDocHistory(fromSnapshot, true);
}
abstract getSpaceDocTimestamps(
spaceId: string,
after?: number
): Promise<Record<string, number> | null>;
abstract listDocHistories(
spaceId: string,
docId: string,
query: { skip?: number; limit?: number }
): Promise<number[]>;
abstract getDocHistory(
spaceId: string,
docId: string,
timestamp: number
): Promise<DocRecord | null>;
// api for internal usage
protected abstract getDocSnapshot(
spaceId: string,
docId: string
): Promise<DocRecord | null>;
protected abstract setDocSnapshot(snapshot: DocRecord): Promise<boolean>;
protected abstract getDocUpdates(
spaceId: string,
docId: string
): Promise<DocUpdate[]>;
protected abstract markUpdatesMerged(
spaceId: string,
docId: string,
updates: DocUpdate[]
): Promise<number>;
protected abstract createDocHistory(
snapshot: DocRecord,
force?: boolean
): Promise<boolean>;
@CallTimer('doc', 'squash')
protected async squash(updates: DocUpdate[]): Promise<DocUpdate> {
const merge = this.options?.mergeUpdates ?? mergeUpdates;
const lastUpdate = updates.at(-1);
if (!lastUpdate) {
throw new Error('No updates to be squashed.');
}
// fast return
if (updates.length === 1) {
return lastUpdate;
}
const finalUpdate = await merge(updates.map(u => u.bin));
return {
bin: finalUpdate,
timestamp: lastUpdate.timestamp,
};
}
protected async lockDocForUpdate(
spaceId: string,
docId: string
): Promise<AsyncDisposable> {
return this.locker.lock(`workspace:${spaceId}:update`, docId);
}
protected generateChangeUpdate(newerBin: Uint8Array, olderBin: Uint8Array) {
const newerDoc = new Doc();
applyUpdate(newerDoc, newerBin);
const olderDoc = new Doc();
applyUpdate(olderDoc, olderBin);
const newerState = encodeStateVector(newerDoc);
const olderState = encodeStateVector(olderDoc);
const diff = encodeStateAsUpdate(newerDoc, olderState);
const undoManager = new UndoManager(Array.from(newerDoc.share.values()));
applyUpdate(olderDoc, diff);
undoManager.undo();
return encodeStateAsUpdate(olderDoc, newerState);
}
}

View File

@@ -0,0 +1,32 @@
// TODO(@forehalo): share with frontend
import type { BlobStorageAdapter } from './blob';
import { Connection } from './connection';
import type { DocStorageAdapter } from './doc';
export class SpaceStorage extends Connection {
constructor(
public readonly doc: DocStorageAdapter,
public readonly blob: BlobStorageAdapter
) {
super();
}
override async connect() {
await this.doc.connect();
await this.blob.connect();
}
override async disconnect() {
await this.doc.disconnect();
await this.blob.disconnect();
}
}
export { BlobStorageAdapter, type BlobStorageOptions } from './blob';
export {
type DocRecord,
DocStorageAdapter,
type DocStorageOptions,
type DocUpdate,
type HistoryFilter,
} from './doc';

View File

@@ -0,0 +1,42 @@
export interface Locker {
lock(domain: string, resource: string): Promise<Lock>;
}
export class SingletonLocker implements Locker {
lockedResource = new Map<string, Lock>();
constructor() {}
async lock(domain: string, resource: string) {
let lock = this.lockedResource.get(`${domain}:${resource}`);
if (!lock) {
lock = new Lock();
}
await lock.acquire();
return lock;
}
}
export class Lock {
private inner: Promise<void> = Promise.resolve();
private release: () => void = () => {};
async acquire() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let release: () => void = null!;
const nextLock = new Promise<void>(resolve => {
release = resolve;
});
await this.inner;
this.inner = nextLock;
this.release = release;
}
[Symbol.asyncDispose]() {
this.release();
return Promise.resolve();
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PermissionService } from './service';
@Module({
providers: [PermissionService],
exports: [PermissionService],
})
export class PermissionModule {}
export { PermissionService } from './service';
export { Permission, PublicPageMode } from './types';

View File

@@ -2,13 +2,12 @@ import { Injectable } from '@nestjs/common';
import type { Prisma } from '@prisma/client';
import { PrismaClient } from '@prisma/client';
import { DocAccessDenied, WorkspaceAccessDenied } from '../../fundamentals';
import { Permission } from './types';
export enum PublicPageMode {
Page,
Edgeless,
}
import {
DocAccessDenied,
SpaceAccessDenied,
SpaceOwnerNotFound,
} from '../../fundamentals';
import { Permission, PublicPageMode } from './types';
@Injectable()
export class PermissionService {
@@ -59,7 +58,7 @@ export class PermissionService {
}
async getWorkspaceOwner(workspaceId: string) {
return this.prisma.workspaceUserPermission.findFirstOrThrow({
const owner = await this.prisma.workspaceUserPermission.findFirst({
where: {
workspaceId,
type: Permission.Owner,
@@ -68,6 +67,12 @@ export class PermissionService {
user: true,
},
});
if (!owner) {
throw new SpaceOwnerNotFound({ spaceId: workspaceId });
}
return owner.user;
}
async tryGetWorkspaceOwner(workspaceId: string) {
@@ -152,7 +157,7 @@ export class PermissionService {
permission: Permission = Permission.Read
) {
if (!(await this.tryCheckWorkspace(ws, user, permission))) {
throw new WorkspaceAccessDenied({ workspaceId: ws });
throw new SpaceAccessDenied({ spaceId: ws });
}
}
@@ -195,6 +200,17 @@ export class PermissionService {
return false;
}
async allowUrlPreview(ws: string) {
const count = await this.prisma.workspace.count({
where: {
id: ws,
public: true,
},
});
return count > 0;
}
async grant(
ws: string,
user: string,
@@ -324,7 +340,7 @@ export class PermissionService {
permission = Permission.Read
) {
if (!(await this.tryCheckPage(ws, page, user, permission))) {
throw new DocAccessDenied({ workspaceId: ws, docId: page });
throw new DocAccessDenied({ spaceId: ws, docId: page });
}
}

View File

@@ -0,0 +1,11 @@
export enum Permission {
Read = 0,
Write = 1,
Admin = 10,
Owner = 99,
}
export enum PublicPageMode {
Page,
Edgeless,
}

View File

@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { FeatureModule } from '../features';
import { PermissionModule } from '../permission';
import { StorageModule } from '../storage';
import { PermissionService } from '../workspaces/permission';
import { QuotaManagementResolver } from './resolver';
import { QuotaService } from './service';
import { QuotaManagementService } from './storage';
@@ -14,13 +14,8 @@ import { QuotaManagementService } from './storage';
* - quota statistics
*/
@Module({
imports: [FeatureModule, StorageModule],
providers: [
PermissionService,
QuotaService,
QuotaManagementResolver,
QuotaManagementService,
],
imports: [FeatureModule, StorageModule, PermissionModule],
providers: [QuotaService, QuotaManagementResolver, QuotaManagementService],
exports: [QuotaService, QuotaManagementService],
})
export class QuotaModule {}

View File

@@ -71,10 +71,6 @@ export class QuotaConfig {
return this.config.configs.historyPeriod;
}
get historyPeriodFromNow() {
return new Date(Date.now() + this.historyPeriod);
}
get memberLimit() {
return this.config.configs.memberLimit;
}

View File

@@ -69,7 +69,7 @@ export class QuotaService {
...quota,
feature: await QuotaConfig.get(this.prisma, quota.featureId),
};
} catch (_) {}
} catch {}
return null as unknown as typeof quota & {
feature: QuotaConfig;
};

View File

@@ -1,9 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceOwnerNotFound } from '../../fundamentals';
import { FeatureService, FeatureType } from '../features';
import { PermissionService } from '../permission';
import { WorkspaceBlobStorage } from '../storage';
import { PermissionService } from '../workspaces/permission';
import { OneGB } from './constant';
import { QuotaService } from './service';
import { formatSize, QuotaQueryType } from './types';
@@ -113,9 +112,7 @@ export class QuotaManagementService {
// get workspace's owner quota and total size of used
// quota was apply to owner's account
async getWorkspaceUsage(workspaceId: string): Promise<QuotaBusinessType> {
const { user: owner } =
await this.permissions.getWorkspaceOwner(workspaceId);
if (!owner) throw new WorkspaceOwnerNotFound({ workspaceId });
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
const {
feature: {
name,

View File

@@ -1,15 +1,15 @@
import { Body, Controller, Post, Req, Res } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import type { Request, Response } from 'express';
import {
ActionForbidden,
EventEmitter,
InternalServerError,
MutexService,
Mutex,
PasswordRequired,
} from '../../fundamentals';
import { AuthService, Public } from '../auth';
import { ServerService } from '../config';
import { UserService } from '../user/service';
interface CreateUserInput {
@@ -20,11 +20,11 @@ interface CreateUserInput {
@Controller('/api/setup')
export class CustomSetupController {
constructor(
private readonly db: PrismaClient,
private readonly user: UserService,
private readonly auth: AuthService,
private readonly event: EventEmitter,
private readonly mutex: MutexService
private readonly mutex: Mutex,
private readonly server: ServerService
) {}
@Public()
@@ -44,7 +44,7 @@ export class CustomSetupController {
throw new InternalServerError();
}
if ((await this.db.user.count()) > 0) {
if (await this.server.initialized()) {
throw new ActionForbidden('First user already created');
}

View File

@@ -0,0 +1,104 @@
import { join } from 'node:path';
import {
Injectable,
Module,
NestMiddleware,
OnModuleInit,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import type { Application, Request, Response } from 'express';
import { static as serveStatic } from 'express';
import { Config } from '../../fundamentals';
import { AuthModule } from '../auth';
import { ServerConfigModule, ServerService } from '../config';
import { UserModule } from '../user';
import { CustomSetupController } from './controller';
@Injectable()
export class SetupMiddleware implements NestMiddleware {
constructor(private readonly server: ServerService) {}
use = (req: Request, res: Response, next: (error?: Error | any) => void) => {
// never throw
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.server
.initialized()
.then(initialized => {
// Redirect to setup page if not initialized
if (!initialized && req.path !== '/admin/setup') {
res.redirect('/admin/setup');
return;
}
// redirect to admin page if initialized
if (initialized && req.path === '/admin/setup') {
res.redirect('/admin');
return;
}
next();
})
.catch(() => {
next();
});
};
}
@Module({
imports: [AuthModule, UserModule, ServerConfigModule],
providers: [SetupMiddleware],
controllers: [CustomSetupController],
})
export class SelfhostModule implements OnModuleInit {
constructor(
private readonly config: Config,
private readonly adapterHost: HttpAdapterHost,
private readonly check: SetupMiddleware
) {}
onModuleInit() {
const staticPath = join(this.config.projectRoot, 'static');
// in command line mode
if (!this.adapterHost.httpAdapter) {
return;
}
const app = this.adapterHost.httpAdapter.getInstance<Application>();
const basePath = this.config.server.path;
app.get(basePath + '/admin/index.html', (_req, res) => {
res.redirect(basePath + '/admin');
});
app.use(
basePath + '/admin',
serveStatic(join(staticPath, 'admin'), {
redirect: false,
index: false,
})
);
app.get(
[basePath + '/admin', basePath + '/admin/*'],
this.check.use,
(_req, res) => {
res.sendFile(join(staticPath, 'admin', 'index.html'));
}
);
app.get(basePath + '/index.html', (_req, res) => {
res.redirect(basePath);
});
app.use(
basePath,
serveStatic(staticPath, {
redirect: false,
index: false,
})
);
app.get('*', this.check.use, (_req, res) => {
res.sendFile(join(staticPath, 'index.html'));
});
}
}

View File

@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth';
import { UserModule } from '../user';
import { CustomSetupController } from './controller';
@Module({
imports: [AuthModule, UserModule],
controllers: [CustomSetupController],
})
export class CustomSetupModule {}

View File

@@ -1,328 +0,0 @@
import { applyDecorators, Logger } from '@nestjs/common';
import {
ConnectedSocket,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage as RawSubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { encodeStateAsUpdate, encodeStateVector } from 'yjs';
import {
CallTimer,
Config,
DocNotFound,
GatewayErrorWrapper,
metrics,
NotInWorkspace,
VersionRejected,
WorkspaceAccessDenied,
} from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { DocManager } from '../../doc';
import { DocID } from '../../utils/doc';
import { PermissionService } from '../../workspaces/permission';
import { Permission } from '../../workspaces/types';
const SubscribeMessage = (event: string) =>
applyDecorators(
GatewayErrorWrapper(event),
CallTimer('socketio', 'event_duration', { event }),
RawSubscribeMessage(event)
);
type EventResponse<Data = any> = Data extends never
? {
data?: never;
}
: {
data: Data;
};
function Sync(workspaceId: string): `${string}:sync` {
return `${workspaceId}:sync`;
}
function Awareness(workspaceId: string): `${string}:awareness` {
return `${workspaceId}:awareness`;
}
@WebSocketGateway()
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
protected logger = new Logger(EventsGateway.name);
private connectionCount = 0;
constructor(
private readonly config: Config,
private readonly docManager: DocManager,
private readonly permissions: PermissionService
) {}
@WebSocketServer()
server!: Server;
handleConnection() {
this.connectionCount++;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
handleDisconnect() {
this.connectionCount--;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
async assertVersion(client: Socket, version?: string) {
const shouldCheckClientVersion = await this.config.runtime.fetch(
'flags/syncClientVersionCheck'
);
if (
// @todo(@darkskygit): remove this flag after 0.12 goes stable
shouldCheckClientVersion &&
version !== AFFiNE.version
) {
client.emit('server-version-rejected', {
currentVersion: version,
requiredVersion: AFFiNE.version,
reason: `Client version${
version ? ` ${version}` : ''
} is outdated, please update to ${AFFiNE.version}`,
});
throw new VersionRejected({
version: version || 'unknown',
serverVersion: AFFiNE.version,
});
}
}
async joinWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.join(room);
}
async leaveWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.leave(room);
}
assertInWorkspace(client: Socket, room: `${string}:${'sync' | 'awareness'}`) {
if (!client.rooms.has(room)) {
throw new NotInWorkspace({ workspaceId: room.split(':')[0] });
}
}
async assertWorkspaceAccessible(
workspaceId: string,
userId: string,
permission: Permission = Permission.Read
) {
if (
!(await this.permissions.isWorkspaceMember(
workspaceId,
userId,
permission
))
) {
throw new WorkspaceAccessDenied({ workspaceId });
}
}
@Auth()
@SubscribeMessage('client-handshake-sync')
async handleClientHandshakeSync(
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
await this.assertVersion(client, version);
await this.assertWorkspaceAccessible(
workspaceId,
user.id,
Permission.Write
);
await this.joinWorkspace(client, Sync(workspaceId));
return {
data: {
clientId: client.id,
},
};
}
@Auth()
@SubscribeMessage('client-handshake-awareness')
async handleClientHandshakeAwareness(
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
await this.assertVersion(client, version);
await this.assertWorkspaceAccessible(
workspaceId,
user.id,
Permission.Write
);
await this.joinWorkspace(client, Awareness(workspaceId));
return {
data: {
clientId: client.id,
},
};
}
@SubscribeMessage('client-leave-sync')
async handleLeaveSync(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
this.assertInWorkspace(client, Sync(workspaceId));
await this.leaveWorkspace(client, Sync(workspaceId));
return {};
}
@SubscribeMessage('client-leave-awareness')
async handleLeaveAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
this.assertInWorkspace(client, Awareness(workspaceId));
await this.leaveWorkspace(client, Awareness(workspaceId));
return {};
}
@SubscribeMessage('client-pre-sync')
async loadDocStats(
@ConnectedSocket() client: Socket,
@MessageBody()
{ workspaceId, timestamp }: { workspaceId: string; timestamp?: number }
): Promise<EventResponse<Record<string, number>>> {
this.assertInWorkspace(client, Sync(workspaceId));
const stats = await this.docManager.getDocTimestamps(
workspaceId,
timestamp
);
return {
data: stats,
};
}
@SubscribeMessage('client-update-v2')
async handleClientUpdateV2(
@MessageBody()
{
workspaceId,
guid,
updates,
}: {
workspaceId: string;
guid: string;
updates: string[];
},
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
this.assertInWorkspace(client, Sync(workspaceId));
const docId = new DocID(guid, workspaceId);
const buffers = updates.map(update => Buffer.from(update, 'base64'));
const timestamp = await this.docManager.batchPush(
docId.workspace,
docId.guid,
buffers
);
client
.to(Sync(workspaceId))
.emit('server-updates', { workspaceId, guid, updates, timestamp });
return {
data: {
accepted: true,
timestamp,
},
};
}
@SubscribeMessage('doc-load-v2')
async loadDocV2(
@ConnectedSocket() client: Socket,
@MessageBody()
{
workspaceId,
guid,
stateVector,
}: {
workspaceId: string;
guid: string;
stateVector?: string;
}
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
> {
this.assertInWorkspace(client, Sync(workspaceId));
const docId = new DocID(guid, workspaceId);
const res = await this.docManager.get(docId.workspace, docId.guid);
if (!res) {
throw new DocNotFound({ workspaceId, docId: docId.guid });
}
const missing = Buffer.from(
encodeStateAsUpdate(
res.doc,
stateVector ? Buffer.from(stateVector, 'base64') : undefined
)
).toString('base64');
const state = Buffer.from(encodeStateVector(res.doc)).toString('base64');
return {
data: {
missing,
state,
timestamp: res.timestamp,
},
};
}
@SubscribeMessage('awareness-init')
async handleInitAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
this.assertInWorkspace(client, Awareness(workspaceId));
client.to(Awareness(workspaceId)).emit('new-client-awareness-init');
return {
data: {
clientId: client.id,
},
};
}
@SubscribeMessage('awareness-update')
async handleHelpGatheringAwareness(
@MessageBody()
{
workspaceId,
awarenessUpdate,
}: { workspaceId: string; awarenessUpdate: string },
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
this.assertInWorkspace(client, Awareness(workspaceId));
client
.to(Awareness(workspaceId))
.emit('server-awareness-broadcast', { workspaceId, awarenessUpdate });
return {};
}
}

View File

@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { DocModule } from '../../doc';
import { PermissionService } from '../../workspaces/permission';
import { EventsGateway } from './events.gateway';
@Module({
imports: [DocModule],
providers: [EventsGateway, PermissionService],
})
export class EventsModule {}

View File

@@ -0,0 +1,661 @@
import { applyDecorators, Logger } from '@nestjs/common';
import {
ConnectedSocket,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage as RawSubscribeMessage,
WebSocketGateway,
} from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { diffUpdate, encodeStateVectorFromUpdate } from 'yjs';
import {
AlreadyInSpace,
CallTimer,
Config,
DocNotFound,
GatewayErrorWrapper,
metrics,
NotInSpace,
SpaceAccessDenied,
VersionRejected,
} from '../../fundamentals';
import { Auth, CurrentUser } from '../auth';
import {
DocStorageAdapter,
PgUserspaceDocStorageAdapter,
PgWorkspaceDocStorageAdapter,
} from '../doc';
import { Permission, PermissionService } from '../permission';
import { DocID } from '../utils/doc';
const SubscribeMessage = (event: string) =>
applyDecorators(
GatewayErrorWrapper(event),
CallTimer('socketio', 'event_duration', { event }),
RawSubscribeMessage(event)
);
type EventResponse<Data = any> = Data extends never
? {
data?: never;
}
: {
data: Data;
};
type RoomType = 'sync' | `${string}:awareness`;
function Room(
spaceId: string,
type: RoomType = 'sync'
): `${string}:${RoomType}` {
return `${spaceId}:${type}`;
}
enum SpaceType {
Workspace = 'workspace',
Userspace = 'userspace',
}
interface JoinSpaceMessage {
spaceType: SpaceType;
spaceId: string;
clientVersion: string;
}
interface JoinSpaceAwarenessMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
clientVersion: string;
}
interface LeaveSpaceMessage {
spaceType: SpaceType;
spaceId: string;
}
interface LeaveSpaceAwarenessMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
}
interface PushDocUpdatesMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
updates: string[];
}
interface LoadDocMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
stateVector?: string;
}
interface LoadDocTimestampsMessage {
spaceType: SpaceType;
spaceId: string;
timestamp?: number;
}
interface LoadSpaceAwarenessesMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
}
interface UpdateAwarenessMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
awarenessUpdate: string;
}
@WebSocketGateway()
export class SpaceSyncGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
protected logger = new Logger(SpaceSyncGateway.name);
private connectionCount = 0;
constructor(
private readonly config: Config,
private readonly permissions: PermissionService,
private readonly workspace: PgWorkspaceDocStorageAdapter,
private readonly userspace: PgUserspaceDocStorageAdapter
) {}
handleConnection() {
this.connectionCount++;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
handleDisconnect() {
this.connectionCount--;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
selectAdapter(client: Socket, spaceType: SpaceType): SyncSocketAdapter {
let adapters: Record<SpaceType, SyncSocketAdapter> = (client as any)
.affineSyncAdapters;
if (!adapters) {
const workspace = new WorkspaceSyncAdapter(
client,
this.workspace,
this.permissions
);
const userspace = new UserspaceSyncAdapter(client, this.userspace);
adapters = { workspace, userspace };
(client as any).affineSyncAdapters = adapters;
}
return adapters[spaceType];
}
async assertVersion(client: Socket, version?: string) {
const shouldCheckClientVersion = await this.config.runtime.fetch(
'flags/syncClientVersionCheck'
);
if (
// @todo(@darkskygit): remove this flag after 0.12 goes stable
shouldCheckClientVersion &&
version !== AFFiNE.version
) {
client.emit('server-version-rejected', {
currentVersion: version,
requiredVersion: AFFiNE.version,
reason: `Client version${
version ? ` ${version}` : ''
} is outdated, please update to ${AFFiNE.version}`,
});
throw new VersionRejected({
version: version || 'unknown',
serverVersion: AFFiNE.version,
});
}
}
async joinWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.join(room);
}
async leaveWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.leave(room);
}
assertInWorkspace(client: Socket, room: `${string}:${'sync' | 'awareness'}`) {
if (!client.rooms.has(room)) {
throw new NotInSpace({ spaceId: room.split(':')[0] });
}
}
// v3
@Auth()
@SubscribeMessage('space:join')
async onJoinSpace(
@CurrentUser() user: CurrentUser,
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, clientVersion }: JoinSpaceMessage
): Promise<EventResponse<{ clientId: string; success: true }>> {
await this.assertVersion(client, clientVersion);
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:leave')
async onLeaveSpace(
@ConnectedSocket() client: Socket,
@MessageBody() { spaceType, spaceId }: LeaveSpaceMessage
): Promise<EventResponse<{ clientId: string; success: true }>> {
await this.selectAdapter(client, spaceType).leave(spaceId);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:load-doc')
async onLoadSpaceDoc(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, docId, stateVector }: LoadDocMessage
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
> {
const adapter = this.selectAdapter(client, spaceType);
adapter.assertIn(spaceId);
const doc = await adapter.get(spaceId, docId);
if (!doc) {
throw new DocNotFound({ spaceId, docId });
}
const missing = Buffer.from(
stateVector
? diffUpdate(doc.bin, Buffer.from(stateVector, 'base64'))
: doc.bin
).toString('base64');
const state = Buffer.from(encodeStateVectorFromUpdate(doc.bin)).toString(
'base64'
);
return {
data: {
missing,
state,
timestamp: doc.timestamp,
},
};
}
@SubscribeMessage('space:push-doc-updates')
async onReceiveDocUpdates(
@ConnectedSocket() client: Socket,
@MessageBody()
message: PushDocUpdatesMessage
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
const { spaceType, spaceId, docId, updates } = message;
const adapter = this.selectAdapter(client, spaceType);
// TODO(@forehalo): we might need to check write permission before push updates
const timestamp = await adapter.push(
spaceId,
docId,
updates.map(update => Buffer.from(update, 'base64'))
);
// could be put in [adapter.push]
// but the event should be kept away from adapter
// so
client
.to(adapter.room(spaceId))
.emit('space:broadcast-doc-updates', { ...message, timestamp });
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
const id = new DocID(docId, spaceId);
client.to(adapter.room(spaceId)).emit('server-updates', {
workspaceId: spaceId,
guid: id.guid,
updates,
timestamp,
});
}
return {
data: {
accepted: true,
timestamp,
},
};
}
@SubscribeMessage('space:load-doc-timestamps')
async onLoadDocTimestamps(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, timestamp }: LoadDocTimestampsMessage
): Promise<EventResponse<Record<string, number>>> {
const adapter = this.selectAdapter(client, spaceType);
const stats = await adapter.getTimestamps(spaceId, timestamp);
return {
data: stats ?? {},
};
}
@Auth()
@SubscribeMessage('space:join-awareness')
async onJoinAwareness(
@ConnectedSocket() client: Socket,
@CurrentUser() user: CurrentUser,
@MessageBody()
{ spaceType, spaceId, docId, clientVersion }: JoinSpaceAwarenessMessage
) {
await this.assertVersion(client, clientVersion);
await this.selectAdapter(client, spaceType).join(
user.id,
spaceId,
`${docId}:awareness`
);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:leave-awareness')
async onLeaveAwareness(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, docId }: LeaveSpaceAwarenessMessage
) {
await this.selectAdapter(client, spaceType).leave(
spaceId,
`${docId}:awareness`
);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:load-awarenesses')
async onLoadAwareness(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, docId }: LoadSpaceAwarenessesMessage
) {
const adapter = this.selectAdapter(client, spaceType);
const roomType = `${docId}:awareness` as const;
adapter.assertIn(spaceId, roomType);
client
.to(adapter.room(spaceId, roomType))
.emit('space:collect-awareness', { spaceType, spaceId, docId });
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
client
.to(adapter.room(spaceId, roomType))
.emit('new-client-awareness-init');
}
return { data: { clientId: client.id } };
}
@SubscribeMessage('space:update-awareness')
async onUpdateAwareness(
@ConnectedSocket() client: Socket,
@MessageBody() message: UpdateAwarenessMessage
) {
const { spaceType, spaceId, docId } = message;
const adapter = this.selectAdapter(client, spaceType);
const roomType = `${docId}:awareness` as const;
adapter.assertIn(spaceId, roomType);
client
.to(adapter.room(spaceId, roomType))
.emit('space:broadcast-awareness-update', message);
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
client
.to(adapter.room(spaceId, roomType))
.emit('server-awareness-broadcast', {
workspaceId: spaceId,
awarenessUpdate: message.awarenessUpdate,
});
}
return {};
}
// TODO(@forehalo): remove
// deprecated section
@Auth()
@SubscribeMessage('client-handshake-sync')
async handleClientHandshakeSync(
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
await this.assertVersion(client, version);
return this.onJoinSpace(user, client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
clientVersion: version,
});
}
@SubscribeMessage('client-leave-sync')
async handleLeaveSync(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onLeaveSpace(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
});
}
@SubscribeMessage('client-pre-sync')
async loadDocStats(
@ConnectedSocket() client: Socket,
@MessageBody()
{ workspaceId, timestamp }: { workspaceId: string; timestamp?: number }
): Promise<EventResponse<Record<string, number>>> {
return this.onLoadDocTimestamps(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
timestamp,
});
}
@SubscribeMessage('client-update-v2')
async handleClientUpdateV2(
@MessageBody()
{
workspaceId,
guid,
updates,
}: {
workspaceId: string;
guid: string;
updates: string[];
},
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
return this.onReceiveDocUpdates(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: guid,
updates,
});
}
@SubscribeMessage('doc-load-v2')
async loadDocV2(
@ConnectedSocket() client: Socket,
@MessageBody()
{
workspaceId,
guid,
stateVector,
}: {
workspaceId: string;
guid: string;
stateVector?: string;
}
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
> {
return this.onLoadSpaceDoc(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: guid,
stateVector,
});
}
@Auth()
@SubscribeMessage('client-handshake-awareness')
async handleClientHandshakeAwareness(
@ConnectedSocket() client: Socket,
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string
): Promise<EventResponse<{ clientId: string }>> {
return this.onJoinAwareness(client, user, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
clientVersion: version,
});
}
@SubscribeMessage('client-leave-awareness')
async handleLeaveAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onLeaveAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
});
}
@SubscribeMessage('awareness-init')
async handleInitAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
return this.onLoadAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
});
}
@SubscribeMessage('awareness-update')
async handleHelpGatheringAwareness(
@MessageBody()
{
workspaceId,
awarenessUpdate,
}: { workspaceId: string; awarenessUpdate: string },
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onUpdateAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
awarenessUpdate,
});
}
}
abstract class SyncSocketAdapter {
constructor(
private readonly spaceType: SpaceType,
public readonly client: Socket,
public readonly storage: DocStorageAdapter
) {}
room(spaceId: string, roomType: RoomType = 'sync') {
return `${this.spaceType}:${Room(spaceId, roomType)}`;
}
async join(userId: string, spaceId: string, roomType: RoomType = 'sync') {
this.assertNotIn(spaceId, roomType);
await this.assertAccessible(spaceId, userId, Permission.Read);
return this.client.join(this.room(spaceId, roomType));
}
async leave(spaceId: string, roomType: RoomType = 'sync') {
this.assertIn(spaceId, roomType);
return this.client.leave(this.room(spaceId, roomType));
}
in(spaceId: string, roomType: RoomType = 'sync') {
return this.client.rooms.has(this.room(spaceId, roomType));
}
assertNotIn(spaceId: string, roomType: RoomType = 'sync') {
if (this.client.rooms.has(this.room(spaceId, roomType))) {
throw new AlreadyInSpace({ spaceId });
}
}
assertIn(spaceId: string, roomType: RoomType = 'sync') {
if (!this.client.rooms.has(this.room(spaceId, roomType))) {
throw new NotInSpace({ spaceId });
}
}
abstract assertAccessible(
spaceId: string,
userId: string,
permission?: Permission
): Promise<void>;
push(spaceId: string, docId: string, updates: Buffer[]) {
this.assertIn(spaceId);
return this.storage.pushDocUpdates(spaceId, docId, updates);
}
get(spaceId: string, docId: string) {
this.assertIn(spaceId);
return this.storage.getDoc(spaceId, docId);
}
getTimestamps(spaceId: string, timestamp?: number) {
this.assertIn(spaceId);
return this.storage.getSpaceDocTimestamps(spaceId, timestamp);
}
}
class WorkspaceSyncAdapter extends SyncSocketAdapter {
constructor(
client: Socket,
storage: DocStorageAdapter,
private readonly permission: PermissionService
) {
super(SpaceType.Workspace, client, storage);
}
override push(spaceId: string, docId: string, updates: Buffer[]) {
const id = new DocID(docId, spaceId);
return super.push(spaceId, id.guid, updates);
}
override get(spaceId: string, docId: string) {
const id = new DocID(docId, spaceId);
return this.storage.getDoc(spaceId, id.guid);
}
async assertAccessible(
spaceId: string,
userId: string,
permission: Permission = Permission.Read
) {
if (
!(await this.permission.isWorkspaceMember(spaceId, userId, permission))
) {
throw new SpaceAccessDenied({ spaceId });
}
}
}
class UserspaceSyncAdapter extends SyncSocketAdapter {
constructor(client: Socket, storage: DocStorageAdapter) {
super(SpaceType.Userspace, client, storage);
}
async assertAccessible(
spaceId: string,
userId: string,
_permission: Permission = Permission.Read
) {
if (spaceId !== userId) {
throw new SpaceAccessDenied({ spaceId });
}
}
}

View File

@@ -1,8 +1,11 @@
import { Module } from '@nestjs/common';
import { EventsModule } from './events/events.module';
import { DocStorageModule } from '../doc';
import { PermissionModule } from '../permission';
import { SpaceSyncGateway } from './gateway';
@Module({
imports: [EventsModule],
imports: [DocStorageModule, PermissionModule],
providers: [SpaceSyncGateway],
})
export class SyncModule {}

View File

@@ -21,7 +21,7 @@ export class DocID {
static parse(raw: string): DocID | null {
try {
return new DocID(raw);
} catch (e) {
} catch {
return null;
}
}

View File

@@ -12,11 +12,10 @@ import {
InvalidHistoryTimestamp,
} from '../../fundamentals';
import { CurrentUser, Public } from '../auth';
import { DocHistoryManager, DocManager } from '../doc';
import { PgWorkspaceDocStorageAdapter } from '../doc';
import { Permission, PermissionService, PublicPageMode } from '../permission';
import { WorkspaceBlobStorage } from '../storage';
import { DocID } from '../utils/doc';
import { PermissionService, PublicPageMode } from './permission';
import { Permission } from './types';
@Controller('/api/workspaces')
export class WorkspacesController {
@@ -24,8 +23,7 @@ export class WorkspacesController {
constructor(
private readonly storage: WorkspaceBlobStorage,
private readonly permission: PermissionService,
private readonly docManager: DocManager,
private readonly historyManager: DocHistoryManager,
private readonly workspace: PgWorkspaceDocStorageAdapter,
private readonly prisma: PrismaClient
) {}
@@ -57,7 +55,7 @@ export class WorkspacesController {
if (!body) {
throw new BlobNotFound({
workspaceId,
spaceId: workspaceId,
blobId: name,
});
}
@@ -97,14 +95,14 @@ export class WorkspacesController {
throw new AccessDenied();
}
const binResponse = await this.docManager.getBinary(
const binResponse = await this.workspace.getDoc(
docId.workspace,
docId.guid
);
if (!binResponse) {
throw new DocNotFound({
workspaceId: docId.workspace,
spaceId: docId.workspace,
docId: docId.guid,
});
}
@@ -126,7 +124,7 @@ export class WorkspacesController {
}
res.setHeader('content-type', 'application/octet-stream');
res.send(binResponse.binary);
res.send(binResponse.bin);
}
@Get('/:id/docs/:guid/histories/:timestamp')
@@ -142,7 +140,7 @@ export class WorkspacesController {
let ts;
try {
ts = new Date(timestamp);
} catch (e) {
} catch {
throw new InvalidHistoryTimestamp({ timestamp });
}
@@ -153,19 +151,19 @@ export class WorkspacesController {
Permission.Write
);
const history = await this.historyManager.get(
const history = await this.workspace.getDocHistory(
docId.workspace,
docId.guid,
ts
ts.getTime()
);
if (history) {
res.setHeader('content-type', 'application/octet-stream');
res.setHeader('cache-control', 'private, max-age=2592000, immutable');
res.send(history.blob);
res.send(history.bin);
} else {
throw new DocHistoryNotFound({
workspaceId: docId.workspace,
spaceId: docId.workspace,
docId: guid,
timestamp: ts.getTime(),
});

View File

@@ -1,13 +1,13 @@
import { Module } from '@nestjs/common';
import { DocModule } from '../doc';
import { DocStorageModule } from '../doc';
import { FeatureModule } from '../features';
import { PermissionModule } from '../permission';
import { QuotaModule } from '../quota';
import { StorageModule } from '../storage';
import { UserModule } from '../user';
import { WorkspacesController } from './controller';
import { WorkspaceManagementResolver } from './management';
import { PermissionService } from './permission';
import {
DocHistoryResolver,
PagePermissionResolver,
@@ -16,17 +16,22 @@ import {
} from './resolvers';
@Module({
imports: [DocModule, FeatureModule, QuotaModule, StorageModule, UserModule],
imports: [
DocStorageModule,
FeatureModule,
QuotaModule,
StorageModule,
UserModule,
PermissionModule,
],
controllers: [WorkspacesController],
providers: [
WorkspaceResolver,
WorkspaceManagementResolver,
PermissionService,
PagePermissionResolver,
DocHistoryResolver,
WorkspaceBlobResolver,
],
exports: [PermissionService],
})
export class WorkspaceModule {}

View File

@@ -12,7 +12,7 @@ import { ActionForbidden } from '../../fundamentals';
import { CurrentUser } from '../auth';
import { Admin } from '../common';
import { FeatureManagementService, FeatureType } from '../features';
import { PermissionService } from './permission';
import { PermissionService } from '../permission';
import { WorkspaceType } from './types';
@Resolver(() => WorkspaceType)
@@ -61,7 +61,7 @@ export class WorkspaceManagementResolver {
const owner = await this.permission.getWorkspaceOwner(workspaceId);
const availableFeatures = await this.availableFeatures(user);
if (owner.user.id !== user.id || !availableFeatures.includes(feature)) {
if (owner.id !== user.id || !availableFeatures.includes(feature)) {
throw new ActionForbidden();
}

View File

@@ -19,10 +19,10 @@ import {
PreventCache,
} from '../../../fundamentals';
import { CurrentUser } from '../../auth';
import { Permission, PermissionService } from '../../permission';
import { QuotaManagementService } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage';
import { PermissionService } from '../permission';
import { Permission, WorkspaceBlobSizes, WorkspaceType } from '../types';
import { WorkspaceBlobSizes, WorkspaceType } from '../types';
@UseGuards(CloudThrottlerGuard)
@Resolver(() => WorkspaceType)

View File

@@ -12,10 +12,10 @@ import {
import type { SnapshotHistory } from '@prisma/client';
import { CurrentUser } from '../../auth';
import { DocHistoryManager } from '../../doc';
import { PgWorkspaceDocStorageAdapter } from '../../doc';
import { Permission, PermissionService } from '../../permission';
import { DocID } from '../../utils/doc';
import { PermissionService } from '../permission';
import { Permission, WorkspaceType } from '../types';
import { WorkspaceType } from '../types';
@ObjectType()
class DocHistoryType implements Partial<SnapshotHistory> {
@@ -32,7 +32,7 @@ class DocHistoryType implements Partial<SnapshotHistory> {
@Resolver(() => WorkspaceType)
export class DocHistoryResolver {
constructor(
private readonly historyManager: DocHistoryManager,
private readonly workspace: PgWorkspaceDocStorageAdapter,
private readonly permission: PermissionService
) {}
@@ -47,17 +47,19 @@ export class DocHistoryResolver {
): Promise<DocHistoryType[]> {
const docId = new DocID(guid, workspace.id);
return this.historyManager
.list(workspace.id, docId.guid, timestamp, take)
.then(rows =>
rows.map(({ timestamp }) => {
return {
workspaceId: workspace.id,
id: docId.guid,
timestamp,
};
})
);
const timestamps = await this.workspace.listDocHistories(
workspace.id,
docId.guid,
{ before: timestamp.getTime(), limit: take }
);
return timestamps.map(timestamp => {
return {
workspaceId: workspace.id,
id: docId.guid,
timestamp: new Date(timestamp),
};
});
}
@Mutation(() => Date)
@@ -76,6 +78,12 @@ export class DocHistoryResolver {
Permission.Write
);
return this.historyManager.recover(docId.workspace, docId.guid, timestamp);
await this.workspace.rollbackDoc(
docId.workspace,
docId.guid,
timestamp.getTime()
);
return timestamp;
}
}

View File

@@ -17,9 +17,13 @@ import {
PageIsNotPublic,
} from '../../../fundamentals';
import { CurrentUser } from '../../auth';
import {
Permission,
PermissionService,
PublicPageMode,
} from '../../permission';
import { DocID } from '../../utils/doc';
import { PermissionService, PublicPageMode } from '../permission';
import { Permission, WorkspaceType } from '../types';
import { WorkspaceType } from '../types';
registerEnumType(PublicPageMode, {
name: 'PublicPageMode',

View File

@@ -15,28 +15,26 @@ import { applyUpdate, Doc } from 'yjs';
import type { FileUpload } from '../../../fundamentals';
import {
CantChangeWorkspaceOwner,
CantChangeSpaceOwner,
EventEmitter,
InternalServerError,
MailService,
MemberQuotaExceeded,
MutexService,
RequestMutex,
SpaceAccessDenied,
SpaceNotFound,
Throttle,
TooManyRequest,
UserNotFound,
WorkspaceAccessDenied,
WorkspaceNotFound,
WorkspaceOwnerNotFound,
} from '../../../fundamentals';
import { CurrentUser, Public } from '../../auth';
import { Permission, PermissionService } from '../../permission';
import { QuotaManagementService, QuotaQueryType } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage';
import { UserService, UserType } from '../../user';
import { PermissionService } from '../permission';
import {
InvitationType,
InviteUserType,
Permission,
UpdateWorkspaceInput,
WorkspaceType,
} from '../types';
@@ -59,7 +57,7 @@ export class WorkspaceResolver {
private readonly users: UserService,
private readonly event: EventEmitter,
private readonly blobStorage: WorkspaceBlobStorage,
private readonly mutex: MutexService
private readonly mutex: RequestMutex
) {}
@ResolveField(() => Permission, {
@@ -78,7 +76,7 @@ export class WorkspaceResolver {
const permission = await this.permissions.get(workspace.id, user.id);
if (!permission) {
throw new WorkspaceAccessDenied({ workspaceId: workspace.id });
throw new SpaceAccessDenied({ spaceId: workspace.id });
}
return permission;
@@ -96,14 +94,27 @@ export class WorkspaceResolver {
});
}
@ResolveField(() => Boolean, {
description: 'is current workspace initialized',
complexity: 2,
})
async initialized(@Parent() workspace: WorkspaceType) {
return this.prisma.snapshot
.count({
where: {
id: workspace.id,
workspaceId: workspace.id,
},
})
.then(count => count > 0);
}
@ResolveField(() => UserType, {
description: 'Owner of workspace',
complexity: 2,
})
async owner(@Parent() workspace: WorkspaceType) {
const data = await this.permissions.getWorkspaceOwner(workspace.id);
return data.user;
return this.permissions.getWorkspaceOwner(workspace.id);
}
@ResolveField(() => [InviteUserType], {
@@ -197,7 +208,7 @@ export class WorkspaceResolver {
const workspace = await this.prisma.workspace.findUnique({ where: { id } });
if (!workspace) {
throw new WorkspaceNotFound({ workspaceId: id });
throw new SpaceNotFound({ spaceId: id });
}
return workspace;
@@ -308,7 +319,7 @@ export class WorkspaceResolver {
);
if (permission === Permission.Owner) {
throw new CantChangeWorkspaceOwner();
throw new CantChangeSpaceOwner();
}
try {
@@ -449,7 +460,7 @@ export class WorkspaceResolver {
avatar: avatar || defaultWorkspaceAvatar,
id: workspaceId,
},
user: owner.user,
user: owner,
invitee: invitee.user,
};
}
@@ -507,12 +518,8 @@ export class WorkspaceResolver {
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
if (!owner.user) {
throw new WorkspaceOwnerNotFound({ workspaceId: workspaceId });
}
if (sendLeaveMail) {
await this.mailer.sendLeaveWorkspaceEmail(owner.user.email, {
await this.mailer.sendLeaveWorkspaceEmail(owner.email, {
workspaceName,
inviteeName: user.name,
});

View File

@@ -11,15 +11,9 @@ import {
import type { Workspace } from '@prisma/client';
import { SafeIntResolver } from 'graphql-scalars';
import { Permission } from '../permission';
import { UserType } from '../user/types';
export enum Permission {
Read = 0,
Write = 1,
Admin = 10,
Owner = 99,
}
registerEnumType(Permission, {
name: 'Permission',
description: 'User permission in workspace',

View File

@@ -17,6 +17,7 @@ export interface PreDefinedAFFiNEConfig {
ENV_MAP: Record<string, ConfigPaths | [ConfigPaths, EnvConfigType?]>;
serverId: string;
serverName: string;
readonly projectRoot: string;
readonly AFFINE_ENV: AFFINE_ENV;
readonly NODE_ENV: NODE_ENV;
readonly version: string;

View File

@@ -1,3 +1,6 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import pkg from '../../../package.json' assert { type: 'json' };
import {
AFFINE_ENV,
@@ -62,6 +65,7 @@ function getPredefinedAFFiNEConfig(): PreDefinedAFFiNEConfig {
affine,
node,
deploy: !node.dev && !node.test,
projectRoot: resolve(fileURLToPath(import.meta.url), '../../../../'),
};
}

View File

@@ -198,6 +198,10 @@ export const USER_FRIENDLY_ERRORS = {
type: 'too_many_requests',
message: 'Too many requests.',
},
not_found: {
type: 'resource_not_found',
message: 'Resource not found.',
},
// User Errors
user_not_found: {
@@ -279,6 +283,10 @@ export const USER_FRIENDLY_ERRORS = {
type: 'invalid_input',
message: 'An invalid email token provided.',
},
link_expired: {
type: 'bad_request',
message: 'The link has expired.',
},
// Authentication & Permission Errors
authentication_required: {
@@ -298,45 +306,49 @@ export const USER_FRIENDLY_ERRORS = {
message: 'You must verify your email before accessing this resource.',
},
// Workspace & Doc & Sync errors
workspace_not_found: {
// Workspace & Userspace & Doc & Sync errors
space_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string' },
message: ({ workspaceId }) => `Workspace ${workspaceId} not found.`,
args: { spaceId: 'string' },
message: ({ spaceId }) => `Space ${spaceId} not found.`,
},
not_in_workspace: {
not_in_space: {
type: 'action_forbidden',
args: { workspaceId: 'string' },
message: ({ workspaceId }) =>
`You should join in workspace ${workspaceId} before broadcasting messages.`,
args: { spaceId: 'string' },
message: ({ spaceId }) =>
`You should join in Space ${spaceId} before broadcasting messages.`,
},
workspace_access_denied: {
already_in_space: {
type: 'action_forbidden',
args: { spaceId: 'string' },
message: ({ spaceId }) => `You have already joined in Space ${spaceId}.`,
},
space_access_denied: {
type: 'no_permission',
args: { workspaceId: 'string' },
message: ({ workspaceId }) =>
`You do not have permission to access workspace ${workspaceId}.`,
args: { spaceId: 'string' },
message: ({ spaceId }) =>
`You do not have permission to access Space ${spaceId}.`,
},
workspace_owner_not_found: {
space_owner_not_found: {
type: 'internal_server_error',
args: { workspaceId: 'string' },
message: ({ workspaceId }) =>
`Owner of workspace ${workspaceId} not found.`,
args: { spaceId: 'string' },
message: ({ spaceId }) => `Owner of Space ${spaceId} not found.`,
},
cant_change_workspace_owner: {
cant_change_space_owner: {
type: 'action_forbidden',
message: 'You are not allowed to change the owner of a workspace.',
message: 'You are not allowed to change the owner of a Space.',
},
doc_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string', docId: 'string' },
message: ({ workspaceId, docId }) =>
`Doc ${docId} under workspace ${workspaceId} not found.`,
args: { spaceId: 'string', docId: 'string' },
message: ({ spaceId, docId }) =>
`Doc ${docId} under Space ${spaceId} not found.`,
},
doc_access_denied: {
type: 'no_permission',
args: { workspaceId: 'string', docId: 'string' },
message: ({ workspaceId, docId }) =>
`You do not have permission to access doc ${docId} under workspace ${workspaceId}.`,
args: { spaceId: 'string', docId: 'string' },
message: ({ spaceId, docId }) =>
`You do not have permission to access doc ${docId} under Space ${spaceId}.`,
},
version_rejected: {
type: 'action_forbidden',
@@ -351,28 +363,36 @@ export const USER_FRIENDLY_ERRORS = {
},
doc_history_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string', docId: 'string', timestamp: 'number' },
message: ({ workspaceId, docId, timestamp }) =>
`History of ${docId} at ${timestamp} under workspace ${workspaceId}.`,
args: { spaceId: 'string', docId: 'string', timestamp: 'number' },
message: ({ spaceId, docId, timestamp }) =>
`History of ${docId} at ${timestamp} under Space ${spaceId}.`,
},
blob_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string', blobId: 'string' },
message: ({ workspaceId, blobId }) =>
`Blob ${blobId} not found in workspace ${workspaceId}.`,
args: { spaceId: 'string', blobId: 'string' },
message: ({ spaceId, blobId }) =>
`Blob ${blobId} not found in Space ${spaceId}.`,
},
expect_to_publish_page: {
type: 'invalid_input',
message: 'Expected to publish a page, not a workspace.',
message: 'Expected to publish a page, not a Space.',
},
expect_to_revoke_public_page: {
type: 'invalid_input',
message: 'Expected to revoke a public page, not a workspace.',
message: 'Expected to revoke a public page, not a Space.',
},
page_is_not_public: {
type: 'bad_request',
message: 'Page is not public.',
},
failed_to_save_updates: {
type: 'internal_server_error',
message: 'Failed to store doc updates.',
},
failed_to_upsert_snapshot: {
type: 'internal_server_error',
message: 'Failed to store doc snapshot.',
},
// Subscription Errors
failed_to_checkout: {

View File

@@ -16,6 +16,12 @@ export class TooManyRequest extends UserFriendlyError {
}
}
export class NotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'not_found', message);
}
}
export class UserNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'user_not_found', message);
@@ -137,6 +143,12 @@ export class InvalidEmailToken extends UserFriendlyError {
}
}
export class LinkExpired extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'link_expired', message);
}
}
export class AuthenticationRequired extends UserFriendlyError {
constructor(message?: string) {
super('authentication_required', 'authentication_required', message);
@@ -161,54 +173,64 @@ export class EmailVerificationRequired extends UserFriendlyError {
}
}
@ObjectType()
class WorkspaceNotFoundDataType {
@Field() workspaceId!: string
class SpaceNotFoundDataType {
@Field() spaceId!: string
}
export class WorkspaceNotFound extends UserFriendlyError {
constructor(args: WorkspaceNotFoundDataType, message?: string | ((args: WorkspaceNotFoundDataType) => string)) {
super('resource_not_found', 'workspace_not_found', message, args);
export class SpaceNotFound extends UserFriendlyError {
constructor(args: SpaceNotFoundDataType, message?: string | ((args: SpaceNotFoundDataType) => string)) {
super('resource_not_found', 'space_not_found', message, args);
}
}
@ObjectType()
class NotInWorkspaceDataType {
@Field() workspaceId!: string
class NotInSpaceDataType {
@Field() spaceId!: string
}
export class NotInWorkspace extends UserFriendlyError {
constructor(args: NotInWorkspaceDataType, message?: string | ((args: NotInWorkspaceDataType) => string)) {
super('action_forbidden', 'not_in_workspace', message, args);
export class NotInSpace extends UserFriendlyError {
constructor(args: NotInSpaceDataType, message?: string | ((args: NotInSpaceDataType) => string)) {
super('action_forbidden', 'not_in_space', message, args);
}
}
@ObjectType()
class WorkspaceAccessDeniedDataType {
@Field() workspaceId!: string
class AlreadyInSpaceDataType {
@Field() spaceId!: string
}
export class WorkspaceAccessDenied extends UserFriendlyError {
constructor(args: WorkspaceAccessDeniedDataType, message?: string | ((args: WorkspaceAccessDeniedDataType) => string)) {
super('no_permission', 'workspace_access_denied', message, args);
export class AlreadyInSpace extends UserFriendlyError {
constructor(args: AlreadyInSpaceDataType, message?: string | ((args: AlreadyInSpaceDataType) => string)) {
super('action_forbidden', 'already_in_space', message, args);
}
}
@ObjectType()
class WorkspaceOwnerNotFoundDataType {
@Field() workspaceId!: string
class SpaceAccessDeniedDataType {
@Field() spaceId!: string
}
export class WorkspaceOwnerNotFound extends UserFriendlyError {
constructor(args: WorkspaceOwnerNotFoundDataType, message?: string | ((args: WorkspaceOwnerNotFoundDataType) => string)) {
super('internal_server_error', 'workspace_owner_not_found', message, args);
export class SpaceAccessDenied extends UserFriendlyError {
constructor(args: SpaceAccessDeniedDataType, message?: string | ((args: SpaceAccessDeniedDataType) => string)) {
super('no_permission', 'space_access_denied', message, args);
}
}
@ObjectType()
class SpaceOwnerNotFoundDataType {
@Field() spaceId!: string
}
export class SpaceOwnerNotFound extends UserFriendlyError {
constructor(args: SpaceOwnerNotFoundDataType, message?: string | ((args: SpaceOwnerNotFoundDataType) => string)) {
super('internal_server_error', 'space_owner_not_found', message, args);
}
}
export class CantChangeWorkspaceOwner extends UserFriendlyError {
export class CantChangeSpaceOwner extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'cant_change_workspace_owner', message);
super('action_forbidden', 'cant_change_space_owner', message);
}
}
@ObjectType()
class DocNotFoundDataType {
@Field() workspaceId!: string
@Field() spaceId!: string
@Field() docId!: string
}
@@ -219,7 +241,7 @@ export class DocNotFound extends UserFriendlyError {
}
@ObjectType()
class DocAccessDeniedDataType {
@Field() workspaceId!: string
@Field() spaceId!: string
@Field() docId!: string
}
@@ -251,7 +273,7 @@ export class InvalidHistoryTimestamp extends UserFriendlyError {
}
@ObjectType()
class DocHistoryNotFoundDataType {
@Field() workspaceId!: string
@Field() spaceId!: string
@Field() docId!: string
@Field() timestamp!: number
}
@@ -263,7 +285,7 @@ export class DocHistoryNotFound extends UserFriendlyError {
}
@ObjectType()
class BlobNotFoundDataType {
@Field() workspaceId!: string
@Field() spaceId!: string
@Field() blobId!: string
}
@@ -291,6 +313,18 @@ export class PageIsNotPublic extends UserFriendlyError {
}
}
export class FailedToSaveUpdates extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'failed_to_save_updates', message);
}
}
export class FailedToUpsertSnapshot extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'failed_to_upsert_snapshot', message);
}
}
export class FailedToCheckout extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'failed_to_checkout', message);
@@ -502,6 +536,7 @@ export class CannotDeleteOwnAccount extends UserFriendlyError {
export enum ErrorNames {
INTERNAL_SERVER_ERROR,
TOO_MANY_REQUEST,
NOT_FOUND,
USER_NOT_FOUND,
USER_AVATAR_NOT_FOUND,
EMAIL_ALREADY_USED,
@@ -520,15 +555,17 @@ export enum ErrorNames {
SIGN_UP_FORBIDDEN,
EMAIL_TOKEN_NOT_FOUND,
INVALID_EMAIL_TOKEN,
LINK_EXPIRED,
AUTHENTICATION_REQUIRED,
ACTION_FORBIDDEN,
ACCESS_DENIED,
EMAIL_VERIFICATION_REQUIRED,
WORKSPACE_NOT_FOUND,
NOT_IN_WORKSPACE,
WORKSPACE_ACCESS_DENIED,
WORKSPACE_OWNER_NOT_FOUND,
CANT_CHANGE_WORKSPACE_OWNER,
SPACE_NOT_FOUND,
NOT_IN_SPACE,
ALREADY_IN_SPACE,
SPACE_ACCESS_DENIED,
SPACE_OWNER_NOT_FOUND,
CANT_CHANGE_SPACE_OWNER,
DOC_NOT_FOUND,
DOC_ACCESS_DENIED,
VERSION_REJECTED,
@@ -538,6 +575,8 @@ export enum ErrorNames {
EXPECT_TO_PUBLISH_PAGE,
EXPECT_TO_REVOKE_PUBLIC_PAGE,
PAGE_IS_NOT_PUBLIC,
FAILED_TO_SAVE_UPDATES,
FAILED_TO_UPSERT_SNAPSHOT,
FAILED_TO_CHECKOUT,
SUBSCRIPTION_ALREADY_EXISTS,
SUBSCRIPTION_NOT_EXISTS,
@@ -574,5 +613,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
[UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidPasswordLengthDataType, WorkspaceNotFoundDataType, NotInWorkspaceDataType, WorkspaceAccessDeniedDataType, WorkspaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const,
[UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const,
});

View File

@@ -40,5 +40,3 @@ export class ErrorModule implements OnModuleInit {
export { UserFriendlyError } from './def';
export * from './errors.gen';
export * from './payment-required';
export * from './too-many-requests';

View File

@@ -1,10 +0,0 @@
import { HttpException, HttpStatus } from '@nestjs/common';
export class PaymentRequiredException extends HttpException {
constructor(desc?: string, code: string = 'Payment Required') {
super(
HttpException.createBody(desc ?? code, code, HttpStatus.PAYMENT_REQUIRED),
HttpStatus.PAYMENT_REQUIRED
);
}
}

View File

@@ -1,14 +0,0 @@
import { HttpException, HttpStatus } from '@nestjs/common';
export class TooManyRequestsException extends HttpException {
constructor(desc?: string, code: string = 'Too Many Requests') {
super(
HttpException.createBody(
desc ?? code,
code,
HttpStatus.TOO_MANY_REQUESTS
),
HttpStatus.TOO_MANY_REQUESTS
);
}
}

View File

@@ -89,7 +89,7 @@ export class URLHelper {
if (!['http:', 'https:'].includes(url.protocol)) return false;
if (!url.hostname) return false;
return true;
} catch (_) {
} catch {
return false;
}
}

View File

@@ -19,7 +19,7 @@ export type { GraphqlContext } from './graphql';
export { CryptoHelper, URLHelper } from './helpers';
export { MailService } from './mailer';
export { CallCounter, CallTimer, metrics } from './metrics';
export { type ILocker, Lock, Locker, MutexService } from './mutex';
export { type ILocker, Lock, Locker, Mutex, RequestMutex } from './mutex';
export {
GatewayErrorWrapper,
getOptionalModuleMetadata,

View File

@@ -1,14 +1,14 @@
import { Global, Module } from '@nestjs/common';
import { Locker } from './local-lock';
import { MutexService } from './mutex';
import { Mutex, RequestMutex } from './mutex';
@Global()
@Module({
providers: [MutexService, Locker],
exports: [MutexService],
providers: [Mutex, RequestMutex, Locker],
exports: [Mutex, RequestMutex],
})
export class MutexModule {}
export { Locker, MutexService };
export { Locker, Mutex, RequestMutex };
export { type Locker as ILocker, Lock } from './lock';

View File

@@ -11,36 +11,11 @@ import { Locker } from './local-lock';
export const MUTEX_RETRY = 5;
export const MUTEX_WAIT = 100;
@Injectable({ scope: Scope.REQUEST })
export class MutexService {
protected logger = new Logger(MutexService.name);
private readonly locker: Locker;
@Injectable()
export class Mutex {
protected logger = new Logger(Mutex.name);
constructor(
@Inject(REQUEST) private readonly request: Request | GraphqlContext,
private readonly ref: ModuleRef
) {
// nestjs will always find and injecting the locker from local module
// so the RedisLocker implemented by the plugin mechanism will not be able to overwrite the internal locker
// we need to use find and get the locker from the `ModuleRef` manually
//
// NOTE: when a `constructor` execute in normal service, the Locker module we expect may not have been initialized
// but in the Service with `Scope.REQUEST`, we will create a separate Service instance for each request
// at this time, all modules have been initialized, so we able to get the correct Locker instance in `constructor`
this.locker = this.ref.get(Locker, { strict: false });
}
protected getId() {
const req = 'req' in this.request ? this.request.req : this.request;
let id = req.headers['x-transaction-id'] as string;
if (!id) {
id = randomUUID();
req.headers['x-transaction-id'] = id;
}
return id;
}
constructor(protected readonly locker: Locker) {}
/**
* lock an resource and return a lock guard, which will release the lock when disposed
@@ -63,10 +38,10 @@ export class MutexService {
* @param key resource key
* @returns LockGuard
*/
async lock(key: string) {
async lock(key: string, owner: string = 'global') {
try {
return await retryable(
() => this.locker.lock(this.getId(), key),
() => this.locker.lock(owner, key),
MUTEX_RETRY,
MUTEX_WAIT
);
@@ -79,3 +54,36 @@ export class MutexService {
}
}
}
@Injectable({ scope: Scope.REQUEST })
export class RequestMutex extends Mutex {
constructor(
@Inject(REQUEST) private readonly request: Request | GraphqlContext,
ref: ModuleRef
) {
// nestjs will always find and injecting the locker from local module
// so the RedisLocker implemented by the plugin mechanism will not be able to overwrite the internal locker
// we need to use find and get the locker from the `ModuleRef` manually
//
// NOTE: when a `constructor` execute in normal service, the Locker module we expect may not have been initialized
// but in the Service with `Scope.REQUEST`, we will create a separate Service instance for each request
// at this time, all modules have been initialized, so we able to get the correct Locker instance in `constructor`
super(ref.get(Locker));
}
protected getId() {
const req = 'req' in this.request ? this.request.req : this.request;
let id = req.headers['x-transaction-id'] as string;
if (!id) {
id = randomUUID();
req.headers['x-transaction-id'] = id;
}
return id;
}
override lock(key: string) {
return super.lock(key, this.getId());
}
}

View File

@@ -1,4 +1,9 @@
import { ArgumentsHost, Catch, Logger } from '@nestjs/common';
import {
ArgumentsHost,
Catch,
Logger,
NotFoundException,
} from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { GqlContextType } from '@nestjs/graphql';
import { ThrottlerException } from '@nestjs/throttler';
@@ -9,6 +14,7 @@ import { Socket } from 'socket.io';
import {
InternalServerError,
NotFound,
TooManyRequest,
UserFriendlyError,
} from '../error';
@@ -19,6 +25,8 @@ export function mapAnyError(error: any): UserFriendlyError {
return error;
} else if (error instanceof ThrottlerException) {
return new TooManyRequest();
} else if (error instanceof NotFoundException) {
return new NotFound();
} else {
const e = new InternalServerError();
e.cause = error;

View File

@@ -123,7 +123,7 @@ export class FsStorageProvider implements StorageProvider {
});
}
}
} catch (e) {
} catch {
// failed to read dir, stop recursion
}
}

View File

@@ -42,7 +42,7 @@ export async function autoMetadata(
}
return metadata;
} catch (e) {
} catch {
return metadata;
}
}

View File

@@ -8,8 +8,8 @@ import {
ThrottlerGuard,
ThrottlerModule,
type ThrottlerModuleOptions,
ThrottlerOptions,
ThrottlerOptionsFactory,
ThrottlerRequest,
ThrottlerStorageService,
} from '@nestjs/throttler';
import type { Request } from 'express';
@@ -74,12 +74,15 @@ export class CloudThrottlerGuard extends ThrottlerGuard {
return `${tracker};${throttler}`;
}
override async handleRequest(
context: ExecutionContext,
limit: number,
ttl: number,
throttlerOptions: ThrottlerOptions
) {
override async handleRequest(request: ThrottlerRequest) {
const {
context,
throttler: throttlerOptions,
ttl,
blockDuration,
} = request;
let limit = request.limit;
// give it 'default' if no throttler is specified,
// so the unauthenticated users visits will always hit default throttler
// authenticated users will directly bypass unprotected APIs in [CloudThrottlerGuard.canActivate]
@@ -118,13 +121,11 @@ export class CloudThrottlerGuard extends ThrottlerGuard {
tracker,
throttlerOptions.name ?? 'default'
);
const { timeToExpire, totalHits } = await this.storageService.increment(
key,
ttl
);
const { timeToExpire, totalHits, isBlocked, timeToBlockExpire } =
await this.storageService.increment(key, ttl, limit, blockDuration, key);
if (totalHits > limit) {
res.header('Retry-After', timeToExpire.toString());
if (isBlocked) {
res.header('Retry-After', timeToBlockExpire.toString());
await this.throwThrottlingException(context, {
limit,
ttl,
@@ -132,6 +133,8 @@ export class CloudThrottlerGuard extends ThrottlerGuard {
tracker,
totalHits,
timeToExpire,
isBlocked,
timeToBlockExpire,
});
}

View File

@@ -472,7 +472,7 @@ export class CopilotController {
if (!body) {
throw new BlobNotFound({
workspaceId,
spaceId: workspaceId,
blobId: key,
});
}

View File

@@ -2,8 +2,8 @@ import './config';
import { ServerFeature } from '../../core/config';
import { FeatureModule } from '../../core/features';
import { PermissionModule } from '../../core/permission';
import { QuotaModule } from '../../core/quota';
import { PermissionService } from '../../core/workspaces/permission';
import { Plugin } from '../registry';
import { CopilotController } from './controller';
import { ChatMessageCache } from './message';
@@ -29,9 +29,8 @@ registerCopilotProvider(OpenAIProvider);
@Plugin({
name: 'copilot',
imports: [FeatureModule, QuotaModule],
imports: [FeatureModule, QuotaModule, PermissionModule],
providers: [
PermissionService,
ChatSessionService,
CopilotResolver,
ChatMessageCache,

View File

@@ -33,7 +33,10 @@ export class ChatPrompt {
private readonly templateParams: PromptParams = {};
static createFromPrompt(
options: Omit<AiPrompt, 'id' | 'createdAt' | 'config'> & {
options: Omit<
AiPrompt,
'id' | 'createdAt' | 'updatedAt' | 'modified' | 'config'
> & {
messages: PromptMessage[];
config: PromptConfig | undefined;
}

View File

@@ -1,8 +1,12 @@
import { Logger } from '@nestjs/common';
import { AiPrompt, PrismaClient } from '@prisma/client';
import { PromptConfig, PromptMessage } from '../types';
type Prompt = Omit<AiPrompt, 'id' | 'createdAt' | 'action' | 'config'> & {
type Prompt = Omit<
AiPrompt,
'id' | 'createdAt' | 'updatedAt' | 'modified' | 'action' | 'config'
> & {
action?: string;
messages: PromptMessage[];
config?: PromptConfig;
@@ -830,7 +834,7 @@ const chat: Prompt[] = [
],
},
{
name: 'chat:gpt4',
name: 'Chat With AFFiNE AI',
model: 'gpt-4o',
messages: [
{
@@ -845,7 +849,20 @@ const chat: Prompt[] = [
export const prompts: Prompt[] = [...actions, ...chat, ...workflows];
export async function refreshPrompts(db: PrismaClient) {
const needToSkip = await db.aiPrompt
.findMany({
where: { modified: true },
select: { name: true },
})
.then(p => p.map(p => p.name));
for (const prompt of prompts) {
// skip prompt update if already modified by admin panel
if (needToSkip.includes(prompt.name)) {
new Logger('CopilotPrompt').warn(`Skip modified prompt: ${prompt.name}`);
return;
}
await db.aiPrompt.upsert({
create: {
name: prompt.name,
@@ -865,6 +882,7 @@ export async function refreshPrompts(db: PrismaClient) {
update: {
action: prompt.action,
model: prompt.model,
updatedAt: new Date(),
messages: {
deleteMany: {},
create: prompt.messages.map((message, idx) => ({

View File

@@ -38,16 +38,11 @@ export class PromptService implements OnModuleInit {
model: true,
config: true,
messages: {
select: {
role: true,
content: true,
params: true,
},
orderBy: {
idx: 'asc',
},
select: { role: true, content: true, params: true },
orderBy: { idx: 'asc' },
},
},
orderBy: { action: { sort: 'asc', nulls: 'first' } },
});
}
@@ -121,11 +116,18 @@ export class PromptService implements OnModuleInit {
.then(ret => ret.id);
}
async update(name: string, messages: PromptMessage[], config?: PromptConfig) {
async update(
name: string,
messages: PromptMessage[],
modifyByApi: boolean = false,
config?: PromptConfig
) {
const { id } = await this.db.aiPrompt.update({
where: { name },
data: {
config: config || undefined,
updatedAt: new Date(),
modified: modifyByApi,
messages: {
// cleanup old messages
deleteMany: {},

View File

@@ -21,12 +21,12 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { CurrentUser } from '../../core/auth';
import { Admin } from '../../core/common';
import { PermissionService } from '../../core/permission';
import { UserType } from '../../core/user';
import { PermissionService } from '../../core/workspaces/permission';
import {
CopilotFailedToCreateMessage,
FileUpload,
MutexService,
RequestMutex,
Throttle,
TooManyRequest,
} from '../../fundamentals';
@@ -265,7 +265,7 @@ export class CopilotType {
export class CopilotResolver {
constructor(
private readonly permissions: PermissionService,
private readonly mutex: MutexService,
private readonly mutex: RequestMutex,
private readonly chatSession: ChatSessionService,
private readonly storage: CopilotStorage
) {}
@@ -517,7 +517,16 @@ export class PromptsManagementResolver {
description: 'List all copilot prompts',
})
async listCopilotPrompts() {
return this.promptService.list();
const prompts = await this.promptService.list();
return prompts.filter(
p =>
p.messages.length > 0 &&
// ignore internal prompts
!p.name.startsWith('workflow:') &&
!p.name.startsWith('debug:') &&
!p.name.startsWith('chat:') &&
!p.name.startsWith('action:')
);
}
@Mutation(() => CopilotPromptType, {
@@ -544,7 +553,7 @@ export class PromptsManagementResolver {
@Args('messages', { type: () => [CopilotPromptMessageType] })
messages: CopilotPromptMessageType[]
) {
await this.promptService.update(name, messages);
await this.promptService.update(name, messages, true);
return this.promptService.get(name);
}
}

View File

@@ -43,7 +43,7 @@ export class CopilotCheckHtmlExecutor extends AutoRegisteredWorkflowExecutor {
}
}
return false;
} catch (e) {
} catch {
return false;
}
}

View File

@@ -34,7 +34,7 @@ export class CopilotCheckJsonExecutor extends AutoRegisteredWorkflowExecutor {
return true;
}
return false;
} catch (e) {
} catch {
return false;
}
}

View File

@@ -21,6 +21,6 @@ import { OAuthService } from './service';
],
controllers: [OAuthController],
contributesTo: ServerFeature.OAuth,
if: config => !!config.plugins.oauth,
if: config => config.flavor.graphql && !!config.plugins.oauth,
})
export class OAuthModule {}

View File

@@ -1,7 +1,7 @@
import 'reflect-metadata';
import { cpSync } from 'node:fs';
import { join } from 'node:path';
import { join, parse } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { config } from 'dotenv';
@@ -12,20 +12,28 @@ import {
} from './fundamentals/config';
import { enablePlugin } from './plugins';
const configDir = join(fileURLToPath(import.meta.url), '../config');
const PROJECT_CONFIG_PATH = join(fileURLToPath(import.meta.url), '../config');
async function loadRemote(remoteDir: string, file: string) {
const filePath = join(configDir, file);
if (configDir !== remoteDir) {
cpSync(join(remoteDir, file), filePath, {
let fileToLoad = join(PROJECT_CONFIG_PATH, file);
if (PROJECT_CONFIG_PATH !== remoteDir) {
const remoteFile = join(remoteDir, file);
const remoteFileAtLocal = join(
PROJECT_CONFIG_PATH,
parse(file).name + '.remote.js'
);
cpSync(remoteFile, remoteFileAtLocal, {
force: true,
});
fileToLoad = remoteFileAtLocal;
}
await import(pathToFileURL(filePath).href);
await import(pathToFileURL(fileToLoad).href);
}
async function load() {
const AFFiNE_CONFIG_PATH = process.env.AFFINE_CONFIG_PATH ?? configDir;
const AFFiNE_CONFIG_PATH =
process.env.AFFINE_CONFIG_PATH ?? PROJECT_CONFIG_PATH;
// Initializing AFFiNE config
//
// 1. load dotenv file to `process.env`
@@ -44,15 +52,22 @@ async function load() {
// TODO(@forehalo):
// Modules may contribute to ENV_MAP, figure out a good way to involve them instead of hardcoding in `./config/affine.env`
// 3. load env => config map to `globalThis.AFFiNE.ENV_MAP
// load local env map as well in case there are new env added
await loadRemote(PROJECT_CONFIG_PATH, 'affine.env.js');
const projectEnvMap = AFFiNE.ENV_MAP;
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.env.js');
const customEnvMap = AFFiNE.ENV_MAP;
AFFiNE.ENV_MAP = { ...projectEnvMap, ...customEnvMap };
// 4. load `config/affine` to patch custom configs
// load local config as well in case there are new default configurations added
await loadRemote(PROJECT_CONFIG_PATH, 'affine.js');
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.js');
// 5. load `config/affine.self` to patch custom configs
// This is the file only take effect in [AFFiNE Cloud]
if (!AFFiNE.isSelfhosted) {
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.self.js');
await loadRemote(PROJECT_CONFIG_PATH, 'affine.self.js');
}
// 6. apply `process.env` map overriding to `globalThis.AFFiNE`

View File

@@ -2,9 +2,13 @@
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type AlreadyInSpaceDataType {
spaceId: String!
}
type BlobNotFoundDataType {
blobId: String!
workspaceId: String!
spaceId: String!
}
enum ChatHistoryOrder {
@@ -175,13 +179,13 @@ input DeleteSessionInput {
type DocAccessDeniedDataType {
docId: String!
workspaceId: String!
spaceId: String!
}
type DocHistoryNotFoundDataType {
docId: String!
spaceId: String!
timestamp: Int!
workspaceId: String!
}
type DocHistoryType {
@@ -192,20 +196,21 @@ type DocHistoryType {
type DocNotFoundDataType {
docId: String!
workspaceId: String!
spaceId: String!
}
union ErrorDataUnion = BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInWorkspaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | VersionRejectedDataType | WorkspaceAccessDeniedDataType | WorkspaceNotFoundDataType | WorkspaceOwnerNotFoundDataType
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | VersionRejectedDataType
enum ErrorNames {
ACCESS_DENIED
ACTION_FORBIDDEN
ALREADY_IN_SPACE
AUTHENTICATION_REQUIRED
BLOB_NOT_FOUND
BLOB_QUOTA_EXCEEDED
CANNOT_DELETE_ALL_ADMIN_ACCOUNT
CANNOT_DELETE_OWN_ACCOUNT
CANT_CHANGE_WORKSPACE_OWNER
CANT_CHANGE_SPACE_OWNER
CANT_UPDATE_LIFETIME_SUBSCRIPTION
COPILOT_ACTION_TAKEN
COPILOT_FAILED_TO_CREATE_MESSAGE
@@ -228,6 +233,8 @@ enum ErrorNames {
EXPECT_TO_PUBLISH_PAGE
EXPECT_TO_REVOKE_PUBLIC_PAGE
FAILED_TO_CHECKOUT
FAILED_TO_SAVE_UPDATES
FAILED_TO_UPSERT_SNAPSHOT
INTERNAL_SERVER_ERROR
INVALID_EMAIL
INVALID_EMAIL_TOKEN
@@ -235,10 +242,12 @@ enum ErrorNames {
INVALID_OAUTH_CALLBACK_STATE
INVALID_PASSWORD_LENGTH
INVALID_RUNTIME_CONFIG_TYPE
LINK_EXPIRED
MAILER_SERVICE_IS_NOT_CONFIGURED
MEMBER_QUOTA_EXCEEDED
MISSING_OAUTH_QUERY_PARAMETER
NOT_IN_WORKSPACE
NOT_FOUND
NOT_IN_SPACE
NO_COPILOT_PROVIDER_AVAILABLE
OAUTH_ACCOUNT_ALREADY_CONNECTED
OAUTH_STATE_EXPIRED
@@ -248,6 +257,9 @@ enum ErrorNames {
SAME_EMAIL_PROVIDED
SAME_SUBSCRIPTION_RECURRING
SIGN_UP_FORBIDDEN
SPACE_ACCESS_DENIED
SPACE_NOT_FOUND
SPACE_OWNER_NOT_FOUND
SUBSCRIPTION_ALREADY_EXISTS
SUBSCRIPTION_EXPIRED
SUBSCRIPTION_HAS_BEEN_CANCELED
@@ -259,9 +271,6 @@ enum ErrorNames {
USER_AVATAR_NOT_FOUND
USER_NOT_FOUND
VERSION_REJECTED
WORKSPACE_ACCESS_DENIED
WORKSPACE_NOT_FOUND
WORKSPACE_OWNER_NOT_FOUND
WRONG_SIGN_IN_CREDENTIALS
WRONG_SIGN_IN_METHOD
}
@@ -409,7 +418,7 @@ type Mutation {
addWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
cancelSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
changeEmail(email: String!, token: String!): UserType!
changePassword(newPassword: String!, token: String!): UserType!
changePassword(newPassword: String!, token: String!, userId: String): Boolean!
"""Cleanup sessions"""
cleanupCopilotSession(options: DeleteSessionInput!): [String!]!
@@ -492,8 +501,8 @@ type Mutation {
verifyEmail(token: String!): Boolean!
}
type NotInWorkspaceDataType {
workspaceId: String!
type NotInSpaceDataType {
spaceId: String!
}
enum OAuthProviderType {
@@ -619,6 +628,9 @@ type SameSubscriptionRecurringDataType {
}
type ServerConfigType {
"""Features for user that can be configured"""
availableUserFeatures: [FeatureType!]!
"""server base url"""
baseUrl: String!
@@ -682,6 +694,18 @@ type ServerServiceConfig {
name: String!
}
type SpaceAccessDeniedDataType {
spaceId: String!
}
type SpaceNotFoundDataType {
spaceId: String!
}
type SpaceOwnerNotFoundDataType {
spaceId: String!
}
type SubscriptionAlreadyExistsDataType {
plan: String!
}
@@ -840,22 +864,10 @@ type VersionRejectedDataType {
version: String!
}
type WorkspaceAccessDeniedDataType {
workspaceId: String!
}
type WorkspaceBlobSizes {
size: SafeInt!
}
type WorkspaceNotFoundDataType {
workspaceId: String!
}
type WorkspaceOwnerNotFoundDataType {
workspaceId: String!
}
type WorkspacePage {
id: String!
mode: PublicPageMode!
@@ -881,6 +893,9 @@ type WorkspaceType {
histories(before: DateTime, guid: String!, take: Int): [DocHistoryType!]!
id: ID!
"""is current workspace initialized"""
initialized: Boolean!
"""member count of workspace"""
memberCount: Int!

View File

@@ -3,8 +3,8 @@ import type { TestFn } from 'ava';
import ava from 'ava';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { createTestingApp } from './utils';
import { buildAppModule } from '../../src/app.module';
import { createTestingApp } from '../utils';
const gql = '/graphql';
@@ -12,15 +12,17 @@ const test = ava as TestFn<{
app: INestApplication;
}>;
test.beforeEach(async t => {
test.before('start app', async t => {
// @ts-expect-error override
AFFiNE.flavor = { type: 'graphql', graphql: true, sync: false };
const { app } = await createTestingApp({
imports: [AppModule],
imports: [buildAppModule()],
});
t.context.app = app;
});
test.afterEach.always(async t => {
test.after.always(async t => {
await t.context.app.close();
});
@@ -55,3 +57,17 @@ test('should init app', async t => {
t.is(config.type, 'Affine');
t.true(Array.isArray(config.features));
});
test('should return 404 for unknown path', async t => {
await request(t.context.app.getHttpServer()).get('/unknown').expect(404);
t.pass();
});
test('should be able to call apis', async t => {
const res = await request(t.context.app.getHttpServer())
.get('/info')
.expect(200);
t.is(res.body.flavor, 'graphql');
});

View File

@@ -0,0 +1,168 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import type { INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';
import request from 'supertest';
import { buildAppModule } from '../../src/app.module';
import { ServerService } from '../../src/core/config';
import { Config } from '../../src/fundamentals';
import { createTestingApp, initTestingDB } from '../utils';
const test = ava as TestFn<{
app: INestApplication;
db: PrismaClient;
}>;
function initTestStaticFiles(staticPath: string) {
mkdirSync(path.join(staticPath, 'admin'), { recursive: true });
const files = {
'index.html': `<!DOCTYPE html><html><body>AFFiNE</body><script src="main.js"/></html>`,
'main.js': `const name = 'affine'`,
'admin/index.html': `<!DOCTYPE html><html><body>AFFiNE Admin</body><script src="/admin/main.js"/></html>`,
'admin/main.js': `const name = 'affine-admin'`,
};
for (const [filename, content] of Object.entries(files)) {
writeFileSync(path.join(staticPath, filename), content);
}
}
test.before('init selfhost server', async t => {
// @ts-expect-error override
AFFiNE.isSelfhosted = true;
const { app } = await createTestingApp({
imports: [buildAppModule()],
});
t.context.app = app;
t.context.db = t.context.app.get(PrismaClient);
const config = app.get(Config);
const staticPath = path.join(config.projectRoot, 'static');
initTestStaticFiles(staticPath);
});
test.beforeEach(async t => {
await initTestingDB(t.context.db);
const server = t.context.app.get(ServerService);
// @ts-expect-error disable cache
server._initialized = false;
});
test.afterEach.always(async t => {
await t.context.app.close();
});
test('do not allow visit index.html directly', async t => {
let res = await request(t.context.app.getHttpServer())
.get('/index.html')
.expect(302);
t.is(res.header.location, '');
res = await request(t.context.app.getHttpServer())
.get('/admin/index.html')
.expect(302);
t.is(res.header.location, '/admin');
});
test('should always return static asset files', async t => {
let res = await request(t.context.app.getHttpServer())
.get('/main.js')
.expect(200);
t.is(res.text, "const name = 'affine'");
res = await request(t.context.app.getHttpServer())
.get('/admin/main.js')
.expect(200);
t.is(res.text, "const name = 'affine-admin'");
await t.context.db.user.create({
data: {
name: 'test',
email: 'test@affine.pro',
},
});
res = await request(t.context.app.getHttpServer())
.get('/main.js')
.expect(200);
t.is(res.text, "const name = 'affine'");
res = await request(t.context.app.getHttpServer())
.get('/admin/main.js')
.expect(200);
t.is(res.text, "const name = 'affine-admin'");
});
test('should be able to call apis', async t => {
const res = await request(t.context.app.getHttpServer())
.get('/info')
.expect(200);
t.is(res.body.flavor, 'allinone');
});
const blockedPages = [
'/',
'/workspace',
'/admin',
'/admin/',
'/admin/accounts',
];
test('should redirect to setup if server is not initialized', async t => {
for (const path of blockedPages) {
const res = await request(t.context.app.getHttpServer()).get(path);
t.is(res.status, 302, `Failed to redirect ${path}`);
t.is(res.header.location, '/admin/setup');
}
t.pass();
});
test('should allow visiting all pages if initialized', async t => {
await t.context.db.user.create({
data: {
name: 'test',
email: 'test@affine.pro',
},
});
for (const path of blockedPages) {
const res = await request(t.context.app.getHttpServer()).get(path);
t.is(res.status, 200, `Failed to visit ${path}`);
}
t.pass();
});
test('should allow visiting setup page if not initialized', async t => {
const res = await request(t.context.app.getHttpServer())
.get('/admin/setup')
.expect(200);
t.true(res.text.includes('AFFiNE Admin'));
});
test('should redirect to admin if initialized', async t => {
await t.context.db.user.create({
data: {
name: 'test',
email: 'test@affine.pro',
},
});
const res = await request(t.context.app.getHttpServer())
.get('/admin/setup')
.expect(302);
t.is(res.header.location, '/admin');
});

View File

@@ -0,0 +1,33 @@
import type { INestApplication } from '@nestjs/common';
import type { TestFn } from 'ava';
import ava from 'ava';
import request from 'supertest';
import { buildAppModule } from '../../src/app.module';
import { createTestingApp } from '../utils';
const test = ava as TestFn<{
app: INestApplication;
}>;
test.before('start app', async t => {
// @ts-expect-error override
AFFiNE.flavor = { type: 'sync', graphql: false, sync: true };
const { app } = await createTestingApp({
imports: [buildAppModule()],
});
t.context.app = app;
});
test.after.always(async t => {
await t.context.app.close();
});
test('should init app', async t => {
const res = await request(t.context.app.getHttpServer())
.get('/info')
.expect(200);
t.is(res.body.flavor, 'sync');
});

View File

@@ -132,13 +132,14 @@ test('set and change password', async t => {
);
const newPassword = randomBytes(16).toString('hex');
const userId = await changePassword(
const success = await changePassword(
app,
u1.token.token,
u1.id,
setPasswordToken as string,
newPassword
);
t.is(u1.id, userId, 'failed to set password');
t.true(success, 'failed to change password');
const ret = auth.signIn(u1Email, newPassword);
t.notThrowsAsync(ret, 'failed to check password');
@@ -201,7 +202,7 @@ test('should revoke token after change user identify', async t => {
await sendSetPasswordEmail(app, u3.token.token, u3Email, 'affine.pro');
const token = await getTokenFromLatestMailMessage();
const newPassword = randomBytes(16).toString('hex');
await changePassword(app, u3.token.token, token as string, newPassword);
await changePassword(app, u3.id, token as string, newPassword);
const user = await currentUser(app, u3.token.token);
t.is(user, null, 'token should be revoked');

View File

@@ -17,6 +17,7 @@ test.afterEach.always(async () => {
test('should be able to get config', t => {
t.true(typeof config.server.host === 'string');
t.is(config.projectRoot, process.cwd());
t.is(config.NODE_ENV, 'test');
});

View File

@@ -1,394 +0,0 @@
import { mock } from 'node:test';
import { TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import test from 'ava';
import * as Sinon from 'sinon';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { DocManager, DocModule } from '../src/core/doc';
import { QuotaModule } from '../src/core/quota';
import { StorageModule } from '../src/core/storage';
import { Config } from '../src/fundamentals/config';
import { createTestingModule } from './utils';
const createModule = () => {
return createTestingModule(
{
imports: [QuotaModule, StorageModule, DocModule],
},
false
);
};
let m: TestingModule;
let timer: Sinon.SinonFakeTimers;
// cleanup database before each test
test.beforeEach(async () => {
timer = Sinon.useFakeTimers({
toFake: ['setInterval'],
});
m = await createModule();
await m.init();
});
test.afterEach.always(async () => {
await m.close();
timer.restore();
});
test('should setup update poll interval', async t => {
const m = await createModule();
const manager = m.get(DocManager);
const fake = mock.method(manager, 'setup');
await m.init();
t.is(fake.mock.callCount(), 1);
// @ts-expect-error private member
t.truthy(manager.job);
m.close();
});
test('should be able to stop poll', async t => {
const manager = m.get(DocManager);
const fake = mock.method(manager, 'destroy');
await m.close();
t.is(fake.mock.callCount(), 1);
// @ts-expect-error private member
t.is(manager.job, null);
});
test('should poll when intervel due', async t => {
const manager = m.get(DocManager);
const interval = m.get(Config).doc.manager.updatePollInterval;
let resolve: any;
// @ts-expect-error private method
const fake = mock.method(manager, 'autoSquash', () => {
return new Promise(_resolve => {
resolve = _resolve;
});
});
timer.tick(interval);
t.is(fake.mock.callCount(), 1);
// busy
timer.tick(interval);
// @ts-expect-error private member
t.is(manager.busy, true);
t.is(fake.mock.callCount(), 1);
resolve();
await timer.tickAsync(1);
// @ts-expect-error private member
t.is(manager.busy, false);
timer.tick(interval);
t.is(fake.mock.callCount(), 2);
});
test('should merge update when intervel due', async t => {
const db = m.get(PrismaClient);
const manager = m.get(DocManager);
const doc = new YDoc();
const text = doc.getText('content');
text.insert(0, 'hello');
const update = encodeStateAsUpdate(doc);
const ws = await db.workspace.create({
data: {
id: '1',
public: false,
},
});
await db.update.createMany({
data: [
{
id: '1',
workspaceId: '1',
blob: Buffer.from([0, 0]),
seq: 1,
},
{
id: '1',
workspaceId: '1',
blob: Buffer.from(update),
seq: 2,
},
],
});
// @ts-expect-error private method
await manager.autoSquash();
t.deepEqual(
(await manager.getBinary(ws.id, '1'))?.binary.toString('hex'),
Buffer.from(update.buffer).toString('hex')
);
let appendUpdate = Buffer.from([]);
doc.on('update', update => {
appendUpdate = Buffer.from(update);
});
text.insert(5, 'world');
await db.update.create({
data: {
workspaceId: ws.id,
id: '1',
blob: appendUpdate,
seq: 3,
},
});
// @ts-expect-error private method
await manager.autoSquash();
t.deepEqual(
(await manager.getBinary(ws.id, '1'))?.binary.toString('hex'),
Buffer.from(encodeStateAsUpdate(doc)).toString('hex')
);
});
test('should have sequential update number', async t => {
const db = m.get(PrismaClient);
const manager = m.get(DocManager);
const doc = new YDoc();
const text = doc.getText('content');
const updates: Buffer[] = [];
doc.on('update', update => {
updates.push(Buffer.from(update));
});
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
await Promise.all(updates.map(update => manager.push('2', '2', update)));
// [1,2,3]
let records = await manager.getUpdates('2', '2');
t.deepEqual(
records.map(({ seq }) => seq),
[1, 2, 3]
);
// @ts-expect-error private method
await manager.autoSquash();
await db.snapshot.update({
where: {
id_workspaceId: {
id: '2',
workspaceId: '2',
},
},
data: {
seq: 0x3ffffffe,
},
});
await Promise.all(updates.map(update => manager.push('2', '2', update)));
records = await manager.getUpdates('2', '2');
// push a new update with new seq num
await manager.push('2', '2', updates[0]);
// let the manager ignore update with the new seq num
const stub = Sinon.stub(manager, 'getUpdates').resolves(records);
// @ts-expect-error private method
await manager.autoSquash();
stub.restore();
records = await manager.getUpdates('2', '2');
// should not merge in one run
t.not(records.length, 0);
});
test('should have correct sequential update number with batching push', async t => {
const manager = m.get(DocManager);
const doc = new YDoc();
const text = doc.getText('content');
const updates: Buffer[] = [];
doc.on('update', update => {
updates.push(Buffer.from(update));
});
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
await manager.batchPush('2', '2', updates);
// [1,2,3]
const records = await manager.getUpdates('2', '2');
t.deepEqual(
records.map(({ seq }) => seq),
[1, 2, 3]
);
});
test('should retry if seq num conflict', async t => {
const manager = m.get(DocManager);
// @ts-expect-error private method
const stub = Sinon.stub(manager, 'getUpdateSeq');
stub.onCall(0).resolves(1);
// seq num conflict
stub.onCall(1).resolves(1);
stub.onCall(2).resolves(2);
await t.notThrowsAsync(() => manager.push('1', '1', Buffer.from([0, 0])));
await t.notThrowsAsync(() => manager.push('1', '1', Buffer.from([0, 0])));
t.is(stub.callCount, 3);
});
test('should throw if meet max retry times', async t => {
const manager = m.get(DocManager);
// @ts-expect-error private method
const stub = Sinon.stub(manager, 'getUpdateSeq');
stub.resolves(1);
await t.notThrowsAsync(() => manager.push('1', '1', Buffer.from([0, 0])));
await t.throwsAsync(
() => manager.push('1', '1', Buffer.from([0, 0]), 3 /* retry 3 times */),
{ message: 'Failed to push update' }
);
t.is(stub.callCount, 5);
});
test('should be able to insert the snapshot if it is new created', async t => {
const manager = m.get(DocManager);
{
const doc = new YDoc();
const text = doc.getText('content');
text.insert(0, 'hello');
const update = encodeStateAsUpdate(doc);
await manager.push('1', '1', Buffer.from(update));
}
const updates = await manager.getUpdates('1', '1');
t.is(updates.length, 1);
// @ts-expect-error private
const { doc } = await manager.squash(null, updates);
t.truthy(doc);
t.is(doc.getText('content').toString(), 'hello');
const restUpdates = await manager.getUpdates('1', '1');
t.is(restUpdates.length, 0);
});
test('should be able to merge updates into snapshot', async t => {
const manager = m.get(DocManager);
const updates: Buffer[] = [];
{
const doc = new YDoc();
doc.on('update', data => {
updates.push(Buffer.from(data));
});
const text = doc.getText('content');
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
text.insert(11, '!');
}
{
await manager.batchPush('1', '1', updates.slice(0, 2));
// do the merge
const { doc } = (await manager.get('1', '1'))!;
t.is(doc.getText('content').toString(), 'helloworld');
}
{
await manager.batchPush('1', '1', updates.slice(2));
const { doc } = (await manager.get('1', '1'))!;
t.is(doc.getText('content').toString(), 'hello world!');
}
const restUpdates = await manager.getUpdates('1', '1');
t.is(restUpdates.length, 0);
});
test('should not update snapshot if doc is outdated', async t => {
const manager = m.get(DocManager);
const db = m.get(PrismaClient);
const updates: Buffer[] = [];
{
const doc = new YDoc();
doc.on('update', data => {
updates.push(Buffer.from(data));
});
const text = doc.getText('content');
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
text.insert(11, '!');
}
await manager.batchPush('2', '1', updates.slice(0, 2)); // 'helloworld'
// merge updates into snapshot
await manager.get('2', '1');
// fake the snapshot is a lot newer
await db.snapshot.update({
where: {
id_workspaceId: {
workspaceId: '2',
id: '1',
},
},
data: {
updatedAt: new Date(Date.now() + 10000),
},
});
{
const snapshot = await manager.getSnapshot('2', '1');
await manager.batchPush('2', '1', updates.slice(2)); // 'hello world!'
const updateRecords = await manager.getUpdates('2', '1');
// @ts-expect-error private
const { doc } = await manager.squash(snapshot, updateRecords);
// all updated will merged into doc not matter it's timestamp is outdated or not,
// but the snapshot record will not be updated
t.is(doc.getText('content').toString(), 'hello world!');
}
{
const doc = new YDoc();
applyUpdate(doc, (await manager.getSnapshot('2', '1'))!.blob);
// the snapshot will not get touched if the new doc's timestamp is outdated
t.is(doc.getText('content').toString(), 'helloworld');
// the updates are known as outdated, so they will be deleted
t.is((await manager.getUpdates('2', '1')).length, 0);
}
});

View File

@@ -0,0 +1,103 @@
import { mock } from 'node:test';
import { TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import test from 'ava';
import * as Sinon from 'sinon';
import { DocStorageModule } from '../../src/core/doc';
import { DocStorageCronJob } from '../../src/core/doc/job';
import { Config } from '../../src/fundamentals/config';
import { createTestingModule } from '../utils';
let m: TestingModule;
let timer: Sinon.SinonFakeTimers;
let db: PrismaClient;
// cleanup database before each test
test.before(async () => {
timer = Sinon.useFakeTimers({
toFake: ['setInterval'],
});
m = await createTestingModule({
imports: [DocStorageModule],
});
db = m.get(PrismaClient);
});
test.after.always(async () => {
await m.close();
timer.restore();
});
test('should poll when intervel due', async t => {
const manager = m.get(DocStorageCronJob);
const interval = m.get(Config).doc.manager.updatePollInterval;
let resolve: any;
const fake = mock.method(manager, 'autoMergePendingDocUpdates', () => {
return new Promise(_resolve => {
resolve = _resolve;
});
});
timer.tick(interval);
t.is(fake.mock.callCount(), 1);
// busy
timer.tick(interval);
// @ts-expect-error private member
t.is(manager.busy, true);
t.is(fake.mock.callCount(), 1);
resolve();
await timer.tickAsync(1);
// @ts-expect-error private member
t.is(manager.busy, false);
timer.tick(interval);
t.is(fake.mock.callCount(), 2);
});
test('should be able to cleanup expired history', async t => {
const timestamp = Date.now();
// insert expired data
await db.snapshotHistory.createMany({
data: Array.from({ length: 10 })
.fill(0)
.map((_, i) => ({
workspaceId: '1',
id: '1',
blob: Buffer.from([1, 1]),
timestamp: new Date(timestamp - 10 - i),
expiredAt: new Date(timestamp - 1),
})),
});
// insert available data
await db.snapshotHistory.createMany({
data: Array.from({ length: 10 })
.fill(0)
.map((_, i) => ({
workspaceId: '1',
id: '1',
blob: Buffer.from([1, 1]),
timestamp: new Date(timestamp + i),
expiredAt: new Date(timestamp + 1000),
})),
});
let count = await db.snapshotHistory.count();
t.is(count, 20);
await m.get(DocStorageCronJob).cleanupExpiredHistory();
count = await db.snapshotHistory.count();
t.is(count, 10);
const example = await db.snapshotHistory.findFirst();
t.truthy(example);
t.true(example!.expiredAt > new Date());
});

View File

@@ -4,35 +4,42 @@ import { PrismaClient } from '@prisma/client';
import test from 'ava';
import * as Sinon from 'sinon';
import { DocHistoryManager } from '../src/core/doc';
import { QuotaModule } from '../src/core/quota';
import { StorageModule } from '../src/core/storage';
import type { EventPayload } from '../src/fundamentals/event';
import { createTestingModule } from './utils';
import {
DocStorageModule,
PgWorkspaceDocStorageAdapter,
} from '../../src/core/doc';
import { DocStorageOptions } from '../../src/core/doc/options';
import { DocRecord } from '../../src/core/doc/storage';
import { createTestingModule, initTestingDB } from '../utils';
let m: TestingModule;
let manager: DocHistoryManager;
let adapter: PgWorkspaceDocStorageAdapter;
let db: PrismaClient;
// cleanup database before each test
test.beforeEach(async () => {
test.before(async () => {
m = await createTestingModule({
imports: [StorageModule, QuotaModule],
providers: [DocHistoryManager],
imports: [DocStorageModule],
});
manager = m.get(DocHistoryManager);
Sinon.stub(manager, 'getExpiredDateFromNow').resolves(
new Date(Date.now() + 1000)
);
adapter = m.get(PgWorkspaceDocStorageAdapter);
db = m.get(PrismaClient);
});
test.afterEach.always(async () => {
await m.close();
test.beforeEach(async () => {
await initTestingDB(db);
const options = m.get(DocStorageOptions);
Sinon.stub(options, 'historyMaxAge').resolves(1000);
});
test.afterEach(async () => {
Sinon.restore();
});
test.after.always(async () => {
await m.close();
});
const snapshot: Snapshot = {
workspaceId: '1',
id: 'doc1',
@@ -43,21 +50,22 @@ const snapshot: Snapshot = {
createdAt: new Date(),
};
function getEventData(
timestamp: Date = new Date()
): EventPayload<'snapshot.updated'> {
function getSnapshot(timestamp: number = Date.now()): DocRecord {
return {
workspaceId: snapshot.workspaceId,
id: snapshot.id,
previous: { ...snapshot, updatedAt: timestamp },
spaceId: snapshot.workspaceId,
docId: snapshot.id,
bin: snapshot.blob,
timestamp,
};
}
test('should create doc history if never created before', async t => {
Sinon.stub(manager, 'last').resolves(null);
// @ts-expect-error private method
Sinon.stub(adapter, 'lastDocHistory').resolves(null);
const timestamp = new Date();
await manager.onDocUpdated(getEventData(timestamp));
const timestamp = Date.now();
// @ts-expect-error private method
await adapter.createDocHistory(getSnapshot(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -67,33 +75,17 @@ test('should create doc history if never created before', async t => {
});
t.truthy(history);
t.is(history?.timestamp.getTime(), timestamp.getTime());
t.is(history?.timestamp.getTime(), timestamp);
});
test('should not create history if timestamp equals to last record', async t => {
const timestamp = new Date();
Sinon.stub(manager, 'last').resolves({ timestamp, state: null });
await manager.onDocUpdated(getEventData(timestamp));
// @ts-expect-error private method
Sinon.stub(adapter, 'lastDocHistory').resolves({ timestamp, state: null });
const history = await db.snapshotHistory.findFirst({
where: {
workspaceId: '1',
id: 'doc1',
},
});
t.falsy(history);
});
test('should not create history if state equals to last record', async t => {
const timestamp = new Date();
Sinon.stub(manager, 'last').resolves({
timestamp: new Date(timestamp.getTime() - 1),
state: snapshot.state,
});
await manager.onDocUpdated(getEventData(timestamp));
// @ts-expect-error private method
await adapter.createDocHistory(getSnapshot(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -107,12 +99,15 @@ test('should not create history if state equals to last record', async t => {
test('should not create history if time diff is less than interval config', async t => {
const timestamp = new Date();
Sinon.stub(manager, 'last').resolves({
// @ts-expect-error private method
Sinon.stub(adapter, 'lastDocHistory').resolves({
timestamp: new Date(timestamp.getTime() - 1000),
state: Buffer.from([0, 1]),
});
await manager.onDocUpdated(getEventData(timestamp));
// @ts-expect-error private method
await adapter.createDocHistory(getSnapshot(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -126,12 +121,15 @@ test('should not create history if time diff is less than interval config', asyn
test('should create history if time diff is larger than interval config and state diff', async t => {
const timestamp = new Date();
Sinon.stub(manager, 'last').resolves({
// @ts-expect-error private method
Sinon.stub(adapter, 'lastDocHistory').resolves({
timestamp: new Date(timestamp.getTime() - 1000 * 60 * 20),
state: Buffer.from([0, 1]),
});
await manager.onDocUpdated(getEventData(timestamp));
// @ts-expect-error private method
await adapter.createDocHistory(getSnapshot(timestamp));
const history = await db.snapshotHistory.findFirst({
where: {
@@ -145,12 +143,15 @@ test('should create history if time diff is larger than interval config and stat
test('should create history with force flag even if time diff in small', async t => {
const timestamp = new Date();
Sinon.stub(manager, 'last').resolves({
// @ts-expect-error private method
Sinon.stub(adapter, 'lastDocHistory').resolves({
timestamp: new Date(timestamp.getTime() - 1),
state: Buffer.from([0, 1]),
});
await manager.onDocUpdated(getEventData(timestamp), true);
// @ts-expect-error private method
await adapter.createDocHistory(getSnapshot(timestamp), true);
const history = await db.snapshotHistory.findFirst({
where: {
@@ -193,31 +194,31 @@ test('should correctly list all history records', async t => {
})),
});
const list = await manager.list(
const list = await adapter.listDocHistories(
snapshot.workspaceId,
snapshot.id,
new Date(timestamp + 20),
8
{ before: timestamp + 20, limit: 8 }
);
const count = await manager.count(snapshot.workspaceId, snapshot.id);
const count = await db.snapshotHistory.count();
t.is(list.length, 8);
t.is(count, 10);
t.is(count, 20);
});
test('should be able to get history data', async t => {
const timestamp = new Date();
const timestamp = Date.now();
await manager.onDocUpdated(getEventData(timestamp), true);
// @ts-expect-error private method
await adapter.createDocHistory(getSnapshot(timestamp), true);
const history = await manager.get(
const history = await adapter.getDocHistory(
snapshot.workspaceId,
snapshot.id,
timestamp
);
t.truthy(history);
t.deepEqual(history?.blob, snapshot.blob);
t.deepEqual(history?.bin, snapshot.blob);
});
test('should be able to get last history record', async t => {
@@ -237,7 +238,11 @@ test('should be able to get last history record', async t => {
})),
});
const history = await manager.last(snapshot.workspaceId, snapshot.id);
// @ts-expect-error private method
const history = await adapter.lastDocHistory(
snapshot.workspaceId,
snapshot.id
);
t.truthy(history);
t.is(history?.timestamp.getTime(), timestamp + 9);
@@ -252,12 +257,14 @@ test('should be able to recover from history', async t => {
},
});
const history1Timestamp = snapshot.updatedAt.getTime() - 10;
await manager.onDocUpdated(getEventData(new Date(history1Timestamp)));
await manager.recover(
// @ts-expect-error private method
await adapter.createDocHistory(getSnapshot(history1Timestamp));
await adapter.rollbackDoc(
snapshot.workspaceId,
snapshot.id,
new Date(history1Timestamp)
history1Timestamp
);
const [history1, history2] = await db.snapshotHistory.findMany({
@@ -272,49 +279,4 @@ test('should be able to recover from history', async t => {
// new history data force created with snapshot state before recovered
t.deepEqual(history2.blob, Buffer.from([1, 1]));
t.deepEqual(history2.state, Buffer.from([1, 1]));
});
test('should be able to cleanup expired history', async t => {
const timestamp = Date.now();
// insert expired data
await db.snapshotHistory.createMany({
data: Array.from({ length: 10 })
.fill(0)
.map((_, i) => ({
workspaceId: snapshot.workspaceId,
id: snapshot.id,
blob: snapshot.blob,
state: snapshot.state,
timestamp: new Date(timestamp - 10 - i),
expiredAt: new Date(timestamp - 1),
})),
});
// insert available data
await db.snapshotHistory.createMany({
data: Array.from({ length: 10 })
.fill(0)
.map((_, i) => ({
workspaceId: snapshot.workspaceId,
id: snapshot.id,
blob: snapshot.blob,
state: snapshot.state,
timestamp: new Date(timestamp + i),
expiredAt: new Date(timestamp + 1000),
})),
});
let count = await db.snapshotHistory.count();
t.is(count, 20);
await manager.cleanupExpiredHistory();
count = await db.snapshotHistory.count();
t.is(count, 10);
const example = await db.snapshotHistory.findFirst();
t.truthy(example);
t.true(example!.expiredAt > new Date());
});

Some files were not shown because too many files have changed in this diff Show More