Skip to content
Merged

Reorg #155

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
# Architecture

At its core, `object-observer` takes a userland object graph and hands back a `Proxy`-wrapped clone (the `Observable`). From that point on, consumers interact **only with the observable**; the original target stays internal. Every mutation on the observable is intercepted by the `Proxy` traps, turned into one or more `Change` events, and dispatched to the registered observers.

```mermaid
classDiagram
class ObservableBase {

}

ObservableBase <|-- ObservableObject
ObservableBase <|-- ObservableArray
ObservableBase <|-- ObservableTypedArray
```
flowchart LR
user["userland code"]

subgraph src["input"]
target["plain object / array /<br/>typed array (target)"]
end

subgraph lib["object-observer"]
direction TB
proxy["Proxy (returned as Observable)"]
meta["oMeta<br/>(target ref, parent,<br/>observers, validators)"]
traps["Proxy traps<br/>set / deleteProperty /<br/>intrinsic methods"]
proxy -.- meta
proxy --> traps
end

observers["observer callbacks"]

user -- "Observable.from(target)" --> target
target -- "cloned & wrapped" --> proxy
proxy -- "returned to user" --> user
user -- "mutates the observable" --> proxy
traps -- "Change[] dispatch" --> observers
user -- "Observable.observe(obs, cb)" --> observers
```

The returned `Observable` is deep: nested objects are themselves observables, sharing the same dispatch pipeline. Mutations anywhere in the graph bubble up through the parent chain so root-level observers see the full rooted path.
6 changes: 5 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [7.0.0]

## [7.0.0] - 2026-04-26
> See the [v6 → v7 migration guide](v6-to-v7-migration-guide.md) for the full list of breaks and before/after examples.

### BREAKING CHANGE
- [Issue no. 152](https://github.com/gullerya/object-observer/issues/152) - `Observable.observe` options API replaced:
- removed: `path`, `pathsOf`, `pathsFrom`
- added: `filters` — a non-empty array of `Filter` instances; multiple filters compose as logical AND
- `Filter` is now exported from the package entry; use its static factories (`exactPaths`, `pathsStartWith`, `directChildrenOf`, `custom`) to build filters
- migration: `{ path: 'a.b' }` → `{ filters: [Filter.exactPaths(['a.b'])] }`; `{ pathsOf: 'a' }` → `{ filters: [Filter.directChildrenOf('a')] }`; `{ pathsFrom: 'a' }` → `{ filters: [Filter.pathsStartWith('a')] }`
- `Change` object fields (`type`, `path`, `value`, `oldValue`, `object`) are now read-only getters (were writable public fields in v6); reading is unaffected, writing throws in strict mode
### Added
- [Issue no. 149](https://github.com/gullerya/object-observer/issues/149) - added verifiers to prevent unallowed changes
- `Filter.directChildrenOf(path)` factory — covers the previous `pathsOf` semantics, with a fixed sibling-prefix bug (`directChildrenOf('inner')` no longer matches `innerX.foo`)
Expand Down
92 changes: 92 additions & 0 deletions docs/v6-to-v7-migration-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Migrating from v6 to v7

`object-observer` v7 is a major release. The core API — `Observable.from`, `Observable.observe`, `Observable.unobserve`, `Observable.isObservable`, `ObjectObserver` — keeps its shape and semantics. The change event model (`type`, `path`, `value`, `oldValue`, `object`) is unchanged.

The only API break is the **observation options** shape: the old path-based options have been replaced with a single `filters` array of `Filter` instances. A handful of smaller, mostly incidental adjustments are listed at the end for completeness.

## Observation options — path-based options replaced by `filters`

### What changed

In v6, `Observable.observe(observable, callback, options)` accepted one of three mutually exclusive path options:

| v6 option | Meaning |
|--------------|------------------------------------------------------|
| `path` | observe one exact path |
| `pathsOf` | observe direct children of a path |
| `pathsFrom` | observe a path and everything below it |

In v7 these three options are **removed**. In their place, `options.filters` accepts a **non-empty array of `Filter` instances**. When multiple filters are provided they compose as logical AND — each narrows the output of the previous one. `Filter` is a new named export; instances are created via static factory methods (the constructor is private).

### Mapping table

| v6 | v7 |
|-----------------------------------------------|----------------------------------------------------------------------|
| `{ path: 'a.b' }` | `{ filters: [Filter.exactPaths(['a.b'])] }` |
| `{ path: ['a.b', 'a.c'] }` *(if ever used)* | `{ filters: [Filter.exactPaths(['a.b', 'a.c'])] }` |
| `{ pathsOf: 'a' }` | `{ filters: [Filter.directChildrenOf('a')] }` |
| `{ pathsOf: '' }` | `{ filters: [Filter.directChildrenOf('')] }` *(root direct children)* |
| `{ pathsFrom: 'a' }` | `{ filters: [Filter.pathsStartWith('a')] }` |
| *(no v6 equivalent)* | `{ filters: [Filter.custom(cs => cs.filter(...))] }` |

### Before / after

```javascript
// v6
import { Observable } from '@gullerya/object-observer';

Observable.observe(obs, cb, { path: 'address.city' });
Observable.observe(obs, cb, { pathsOf: 'address' });
Observable.observe(obs, cb, { pathsFrom: 'address' });
```

```javascript
// v7
import { Observable, Filter } from '@gullerya/object-observer';

Observable.observe(obs, cb, { filters: [Filter.exactPaths(['address.city'])] });
Observable.observe(obs, cb, { filters: [Filter.directChildrenOf('address')] });
Observable.observe(obs, cb, { filters: [Filter.pathsStartWith('address')] });
```

### Behavior notes

- **`directChildrenOf` fixes a v6 sibling-prefix bug.** In v6, `pathsOf: 'inner'` could incorrectly match changes under a sibling named `innerX` (see [issue #93](https://github.com/gullerya/object-observer/issues/93) for the original fix attempt). `Filter.directChildrenOf('inner')` in v7 matches only true direct children of `'inner'`. If your v6 code relied on the buggy behavior, update the expected path.
- **`REVERSE` / `SHUFFLE` at the target path are still delivered by `directChildrenOf`**, matching v6 `pathsOf` semantics — they are semantically mutations of the container's children.
- **Multiple filters compose as AND.** There was no way to combine path options in v6; in v7 you can stack filters to narrow further.
- **Bare functions are rejected.** `{ filters: [(cs) => cs] }` throws. Wrap with `Filter.custom(fn)`.
- **Unknown options still throw, as in v6.** `Observable.observe` continues to fail-fast on unrecognized option keys — only the valid key name changed.

See [docs/filters.md](filters.md) for the full `Filter` API.

## Other breaking changes

### `Change` objects are read-only

In v6 the `Change` class exposed its fields as plain writable properties. In v7, `type`, `path`, `value`, `oldValue`, and `object` are read-only getters backed by private fields. Code that **read** change properties is unaffected. Code that **wrote** to change objects — e.g. `change.type = 'custom'` or attaching ad-hoc properties to a change — will throw in strict mode (and silently fail in sloppy mode). This is very rarely done in practice; if you were using change objects as a scratch space, copy the fields into your own object instead.

A new `pathAsString` getter is available (lazy, dot-joined). Additive, not breaking.

### Runtime and browser matrix

The officially supported matrix has been refreshed:

- Node.js: **24.15.0+** (aligned with current LTS).
- Browsers: **last 2 versions** of Chrome, Firefox, Edge, and Safari.

This is the matrix the library is actively **tested against** — it is not a hard runtime requirement. The library does not use any v7-specific new language or platform features, so older Node.js versions and older browsers will most likely continue to work just fine. We simply don't run CI against them anymore, so regressions there won't be caught by us.

## What did **not** change

- `Observable.from`, `Observable.isObservable`, `Observable.observe`, `Observable.unobserve` signatures.
- `ObjectObserver` class and its `observe` / `unobserve` / `disconnect` methods.
- The `Change` shape (`type`, `path`, `value`, `oldValue`, `object`) and the set of change types (`insert`, `update`, `delete`, `shuffle`, `reverse`).
- Synchronous default delivery and the opt-in `async: true` option on `Observable.from`.
- The set of observed mutations on plain objects, arrays, and typed arrays.
- Cross-instance operability.

## New in v7 (non-breaking, for awareness)

- **`Filter` is exported**, with static factories: `exactPaths`, `pathsStartWith`, `directChildrenOf`, `custom`.
- **`Validator` is exported**, with a `Validator.custom(fn)` factory. Validators are passed via `Observable.from(target, { validators: [...] })` and run **before** a mutation is applied — throwing from a validator vetoes the mutation (the target is not modified and observers are not invoked). See the `Validators` section in the readme for details.
> In 7.0 validators do not yet run for `Array.prototype.splice`, `Array.prototype.fill`, and `Array.prototype.copyWithin` (and their `TypedArray` counterparts where applicable). Support is planned for a subsequent release.
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

__`object-observer`__ provides a deep observation of a changes performed on an object/array graph.

For a high-level overview of how the library is put together see the [architecture](docs/architecture.md) doc.

Main aspects and features:
- implemented via native __Proxy__ (revokable)
- observation is 'deep', yielding changes from a __sub-graphs__ too
Expand Down
Loading