Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
18ed918
Initial prototype of the observable state tree package
drborges Mar 24, 2025
7319d90
Implement basis of OST lazy initialization
drborges Mar 29, 2025
e349844
Apply node variable naming convention
drborges Mar 30, 2025
893777e
Implement test matcher to make it easier to check if a node exists in…
drborges Mar 30, 2025
02c93f1
Remove unnecessary ost folder
drborges Mar 30, 2025
3c91a91
Binds 'this' in methods to the corresponding OST node
drborges Mar 30, 2025
efdcf86
Handle 'set' mutations triggered by an OST node
drborges Mar 30, 2025
ef2e87e
Attempt to recursively type state tree nodes
drborges Mar 30, 2025
d79b184
Add test coverage to Node# iterator
drborges Mar 30, 2025
d89383f
Allow passing OST root state via constructor
drborges Mar 30, 2025
a3ded7d
Polish subscription system
drborges Mar 31, 2025
181ec4c
Implement object handler's delete trap
drborges Apr 1, 2025
dbc5117
Add support to custom types and getters
drborges Apr 3, 2025
0fa4e82
Allow detached class properties
drborges Apr 3, 2025
aa2f167
Implement Node's Symbol.toStringTag
drborges Apr 4, 2025
1260ed4
Return mutated node from OST#mutate
drborges Apr 5, 2025
442b488
Create mutation helpers to make it easier to reuse common opeartions
drborges Apr 5, 2025
271c5e3
Implement $array handler
drborges Apr 5, 2025
bb2c80f
Implement visitor pattern to make it easier to extend OST proxying me…
drborges Apr 5, 2025
71b1643
Implement pop visitor for handling Array#pop
drborges Apr 7, 2025
6b4f200
Implement shift visitor for handling Array#shift
drborges Apr 7, 2025
8916b7a
Implement unshift visitor for handling Array#unshift
drborges Apr 8, 2025
370abf7
Improve subscription metadata API
drborges Apr 8, 2025
3d9b462
Add .tool-versions
drborges Apr 9, 2025
e3cf660
Implement reverse visitor for handling Array#reverse
drborges Apr 11, 2025
9e5759b
Simplify OST subscription metadata
drborges Apr 12, 2025
fe9f8f8
Implement copyWithin visitor for handling Array#copyWithin
drborges Apr 12, 2025
94e6b72
Ensure visitors are created only once
drborges Jun 7, 2025
fb90aa9
Implement test coverage for Array#splice
drborges Jun 7, 2025
8085526
Implement Array#fill visitor
drborges Jun 23, 2025
8fc2237
Fix type declarations of vitest custom matchers
drborges Jun 23, 2025
59095d9
Remove unnecessary console.log
drborges Jun 25, 2025
e8a1b6d
Apply small optimizations
drborges Jun 25, 2025
ee44a4f
Fix test coverage for Node#
drborges Jun 25, 2025
97295a9
Remove bad conflict resolution
drborges Jun 26, 2025
f0317fc
Add support to Map nodes
drborges Jun 28, 2025
c16931f
Add some more test coverage to array handler
drborges Jun 28, 2025
bd7b002
Optimize visitor matching logic
drborges Jun 28, 2025
6f1ece7
Avoid redundant childValue resolution
drborges Jun 28, 2025
3da4f3f
Move proxiable logic closer together for better locality of behavior
drborges Jun 29, 2025
2a48daf
Add support to Set nodes
drborges Jun 29, 2025
7973094
Implement remaining of Set visitors via Claude's agentic mode
drborges Jun 29, 2025
16ffc9a
Fix linting and TS errors around recently added Set API spec changes
drborges Jun 29, 2025
a0fac4b
Increase test coverage for array nodes
drborges Jun 29, 2025
c8a45f9
Add support to WeakSet and WeakMap as OST nodes
drborges Jun 30, 2025
400c588
Organize handlers and visitors to make codebase navigation easier
drborges Jul 1, 2025
23b4db3
Remove unnecessary indirection on mutation logic
drborges Jul 1, 2025
6368b25
Clean up code and remove some duplication
drborges Jul 2, 2025
8c372be
Allow traversing of Map and Set nodes
drborges Jul 5, 2025
62c7306
Implement foundation for OST scoping
drborges Jul 5, 2025
fbea5d6
Implement scope handlers for better encapsulation
drborges Jul 8, 2025
04e5700
WIP
drborges Jul 14, 2025
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
8 changes: 8 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["dist"],
"extends": ["prettier", "plugin:@typescript-eslint/recommended"],
"parserOptions": {
"ecmaVersion": 2023,
"sourceType": "module"
},
"env": {
"es2022": true,
"node": true
},
"rules": {
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
Expand Down
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nodejs 23.11.0
yarn 1.22.22
Binary file modified .yarn/install-state.gz
Binary file not shown.
5 changes: 5 additions & 0 deletions .zed/debug.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"turbo": "^1.9.4",
"typescript": "^5.0.4"
"typescript": "^5.5.0"
}
}
7 changes: 7 additions & 0 deletions packages/arbor-ost/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["perf"],
"parserOptions": {
"project": ["./tsconfig.eslint.json"]
}
}
2 changes: 2 additions & 0 deletions packages/arbor-ost/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
**/dist/
64 changes: 64 additions & 0 deletions packages/arbor-ost/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "@arborjs/ost",
"sideEffects": false,
"version": "0.0.1-alpha.1",
"description": "An observable state tree data structure used to power @arborjs/store.",
"keywords": [
"arbor",
"react",
"state",
"store",
"typescript",
"tree"
],
"repository": {
"url": "https://github.com/drborges/arbor",
"type": "git"
},
"bugs": {
"url": "https://github.com/drborges/arbor/issues",
"email": "drborges.cic@gmail.com"
},
"author": {
"email": "drborges.cic@gmail.com",
"name": "Diego Borges",
"url": "https://github.com/drborges"
},
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
".": {
"require": "./dist/index.cjs.js",
"import": "./dist/index.esm.js",
"default": "./dist/index.esm.js"
}
},
"license": "MIT",
"prettier": "@arborjs/config-prettier",
"files": [
"./dist",
"./README",
"../../LICENSE"
],
"publishConfig": {
"access": "public"
},
"devDependencies": {
"esbuild": "^0.17.19",
"eslint": "^8.40.0",
"prettier": "^2.8.8",
"typescript": "^5.0.4",
"vitest": "^1.6.0"
},
"scripts": {
"prettier": "prettier -w . ",
"clean": "rm -rf dist",
"lint": "yarn eslint . --ext .js,.jsx,.ts,.tsx",
"test": "vitest run",
"dev": "vitest -w",
"build": "yarn clean && NODE_ENV=production node ../../tools/build.js && yarn tsc",
"build:dev": "yarn clean && NODE_ENV=development node ../../tools/build.js && yarn tsc"
}
}
7 changes: 7 additions & 0 deletions packages/arbor-ost/src/decorators/detached.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const ArborDetached = Symbol.for("ArborDetached")

export function detached(target: unknown, prop: unknown) {
target[ArborDetached] = target[ArborDetached] || {}
const detachedProps = target[ArborDetached]
detachedProps[prop] = true
}
17 changes: 17 additions & 0 deletions packages/arbor-ost/src/decorators/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const ArborProxiable = Symbol.for("ArborProxiable")

/**
* Make application classes eligible for being used as nodes in the Arbor state tree.
*
* @example
*
* ```ts
* @node
* class Todo {
* text: string
* }
* ```
*/
export function node<T extends Function>(target: T, _context: unknown = null) {
target.prototype[ArborProxiable] = true
}
9 changes: 9 additions & 0 deletions packages/arbor-ost/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class ArborError extends Error {}

export class DetachedPathError extends ArborError {
constructor(humanizedPath: string) {
super(
`Path ${humanizedPath} is detached from the tree. Cannot perform mutation.`
)
}
}
32 changes: 32 additions & 0 deletions packages/arbor-ost/src/handlers/$array/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { OST } from "../../ost"
import { $object } from "./../$object"
import { PopVisitor } from "./visitors/pop"
import { PushVisitor } from "./visitors/push"
import { ShiftVisitor } from "./visitors/shift"
import { SpliceVisitor } from "./visitors/splice"
import { UnshiftVisitor } from "./visitors/unshift"
import { ReverseVisitor } from "./visitors/reverse"
import { CopyWithinVisitor } from "./visitors/copyWithin"
import { FillVisitor } from "./visitors/fill"
import { Visitors } from "../visitors"

const visitors = new Visitors(
new PushVisitor(),
new PopVisitor(),
new ShiftVisitor(),
new UnshiftVisitor(),
new ReverseVisitor(),
new SpliceVisitor(),
new CopyWithinVisitor(),
new FillVisitor()
)

export class $array extends $object {
constructor(ost: OST) {
super(ost, visitors)
}

static accepts(value: unknown): boolean {
return Array.isArray(value)
}
}
18 changes: 18 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/copyWithin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Visitor } from "../../visitor"

export class CopyWithinVisitor extends Visitor {
prop = "copyWithin"

visit({ ost, target, $node }) {
return (...args: unknown[]) => {
return ost.mutate($node, () => {
target.copyWithin(...args)

return {
args: args,
operation: "copyWithin",
}
})
}
}
}
18 changes: 18 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/fill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Visitor } from "../../visitor"

export class FillVisitor extends Visitor {
prop = "fill"

visit({ ost, target, $node }) {
return (...args: unknown[]) => {
return ost.mutate($node, () => {
target.fill(...args)

return {
args: args,
operation: "fill",
}
})
}
}
}
22 changes: 22 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/pop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Visitor } from "../../visitor"

export class PopVisitor extends Visitor {
prop = "pop"

visit({ ost, target, $node }) {
return () => {
let popped: unknown

ost.mutate($node, () => {
popped = target.pop()

return {
args: [],
operation: "pop",
}
})

return popped
}
}
}
23 changes: 23 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Visitor } from "../../visitor"
import { Value } from "../../../types"

export class PushVisitor extends Visitor {
prop = "push"

visit({ ost, target, $node }) {
return (...items: Value[]) => {
let length: number

ost.mutate($node, () => {
length = target.push(...items)

return {
args: [items],
operation: "push",
}
})

return length
}
}
}
18 changes: 18 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/reverse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Visitor } from "../../visitor"

export class ReverseVisitor extends Visitor {
prop = "reverse"

visit({ ost, target, $node }) {
return () => {
return ost.mutate($node, () => {
target.reverse()

return {
args: [],
operation: "reverse",
}
})
}
}
}
22 changes: 22 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/shift.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Visitor } from "../../visitor"

export class ShiftVisitor extends Visitor {
prop = "shift"

visit({ ost, target, $node }) {
return () => {
let removed: unknown

ost.mutate($node, () => {
removed = target.shift()

return {
args: [],
operation: "shift",
}
})

return removed
}
}
}
26 changes: 26 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/splice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Visitor } from "../../visitor"

export class SpliceVisitor extends Visitor {
prop = "splice"

visit({ ost, target, $node }) {
return (
start: number,
deleteCount: number,
...items: unknown[]
): unknown[] => {
let deleted: unknown[]

ost.mutate($node, () => {
deleted = target.splice(start, deleteCount, ...items)

return {
args: [start, deleteCount, items],
operation: "splice",
}
})

return deleted
}
}
}
22 changes: 22 additions & 0 deletions packages/arbor-ost/src/handlers/$array/visitors/unshift.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Visitor } from "../../visitor"

export class UnshiftVisitor extends Visitor {
prop = "unshift"

visit({ ost, target, $node }) {
return (...args: unknown[]) => {
let unshifted: unknown[]
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unshift method returns a number (the new length), but the variable is typed as unknown[]. The assignment should be unshifted = target.unshift(...args) and the return type should be number.

Suggested change
let unshifted: unknown[]
let unshifted: number

Copilot uses AI. Check for mistakes.

ost.mutate($node, () => {
unshifted = target.unshift(...args)

return {
args: [args],
operation: "unshift",
}
})

return unshifted
}
}
}
40 changes: 40 additions & 0 deletions packages/arbor-ost/src/handlers/$map/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { OST } from "../../ost"
import { $object } from "./../$object"
import { Visitors } from "../visitors"
import { GetVisitor } from "./visitors/get"
import { SetVisitor } from "./visitors/set"
import { SizeVisitor } from "./visitors/size"
import { DeleteVisitor } from "./visitors/delete"
import { ClearVisitor } from "./visitors/clear"
import { EntriesVisitor } from "./visitors/entries"
import { ForEachVisitor } from "./visitors/forEach"
import { HasVisitor } from "./visitors/has"
import { KeysVisitor } from "./visitors/keys"
import { ValuesVisitor } from "./visitors/values"
import { IteratorVisitor } from "./visitors/iterator"
import { ChildrenVisitor } from "./visitors/$children"

const visitors = new Visitors(
new ChildrenVisitor(),
new GetVisitor(),
new SetVisitor(),
new SizeVisitor(),
new DeleteVisitor(),
new ClearVisitor(),
new EntriesVisitor(),
new ForEachVisitor(),
new HasVisitor(),
new KeysVisitor(),
new ValuesVisitor(),
new IteratorVisitor()
)

export class $map extends $object {
constructor(ost: OST) {
super(ost, visitors)
}

static accepts(value: unknown): boolean {
return value instanceof Map
}
}
Loading
Loading