Gathering detailed insights and metrics for ts-state-graph
Gathering detailed insights and metrics for ts-state-graph
Gathering detailed insights and metrics for ts-state-graph
Gathering detailed insights and metrics for ts-state-graph
@ty-ras/state-io-ts
[![Coverage](https://codecov.io/gh/ty-ras/data-io-ts/branch/main/graph/badge.svg?flag=state)](https://codecov.io/gh/ty-ras/data-io-ts)
@omegion1npm/voluptatibus-facere-incidunt
[![codecov](https://codecov.io/gh/SalmonMode/@omegion1npm/voluptatibus-facere-incidunt/branch/main/graph/badge.svg?token=E28MMT0TC6)](https://codecov.io/gh/SalmonMode/@omegion1npm/voluptatibus-facere-incidunt) [![Build](https://github.com/omegion1npm/volu
npm install ts-state-graph
Typescript
Module System
Node Version
NPM Version
66.8
Supply Chain
94.2
Quality
76
Maintenance
100
Vulnerability
100
License
Total Downloads
505
Last Day
2
Last Week
15
Last Month
17
Last Year
213
Minified
Minified + Gzipped
Latest Version
0.2.0
Package Id
ts-state-graph@0.2.0
Unpacked Size
196.75 kB
Size
31.07 kB
File Count
22
NPM Version
10.2.3
Node Version
18.19.0
Publised On
05 Feb 2024
Cumulative downloads
Total Downloads
Last day
0%
2
Compared to previous day
Last week
1,400%
15
Compared to previous week
Last month
183.3%
17
Compared to previous month
Last year
-27.1%
213
Compared to previous year
3
6
ts-state-graph organises your application state as a graph, but stores it as a normalised pool of entities. This makes it is easier to automatically replicate state to your backend and between clients, without needing to treat it like a single document.
You'll get a two way data flow that looks like
client A: frontend mutates graph -> mutates pool -> mutates server
client B: server applies mutation -> updates pool -> updates graph
Now you can build multiplayer, client first, web apps while you enjoy a relatively familiar looking API on your server, and reactive state management on your client.
This is the approach described by Linear for their client side state management when discussing their sync engine. Except they use classes and decorators, and we use runtime types and type inference. If you already use zod in your API (i.e. trpc or ts-rest) this may be a good fit for you ;)
Let's have a look. Or alternatively checkout the playground.
1//describe your entities 2//(if your api is already defined with zod you could reuse them here) 3export const chatRoomModel = model({ 4 name: 'ChatRoom', 5 shape: { 6 id: z.string(), 7 ownerId: z.string(), 8 }, 9}); 10 11export const userModel = model({ 12 name: 'User', 13 shape: { 14 id: z.string(), 15 name: z.string(), 16 roomId: z.string(), 17 }, 18}); 19 20export const messageModel = model({ 21 name: 'Message', 22 shape: { 23 id: z.string(), 24 text: z.string(), 25 authorId: z.string(), 26 recipientId: z.string(), 27 order: z.number(), 28 roomId: z.string(), 29 }, 30}); 31 32//describe relationships between your entities 33const chatRoomOwnerRel = oneToOne( 34 source(chatRoomModel, 'ownerId').auto(), 35 target(userModel), 36); 37 38const userRoomRel = manyToOne( 39 source(userModel, 'roomId').auto(), 40 target(chatRoomModel).as('users'), 41); 42 43const authorRel = manyToOne( 44 source(messageModel, 'authorId').auto(), 45 target(userModel).as('outbox'), 46); 47 48const recipientRel = manyToOne( 49 source(messageModel, 'recipientId').auto(), 50 target(userModel).as('inbox'), 51); 52 53const messageRoomRel = manyToOne( 54 source(messageModel, 'roomId').auto(), 55 target(chatRoomModel).as('messages'), 56); 57 58//construct views by attaching relations to your entities 59const messageView = view(messageModel).outgoing([ 60 authorRel, 61 recipientRel, 62 messageRoomRel, 63]); 64 65type MessageView = InferView<typeof messageView>; 66/* 67inferred as 68{ 69 id: string; 70 roomId: string; 71 text: string; 72 authorId: string; 73 recipientId: string; 74 order: number; 75 author: { 76 id: string; 77 name: string; 78 roomId: string; 79 80 //in reality this is a generic function, it doesn't actually 81 //know the return type untill you pass in the view. 82 //this is how we get around type inference problems 83 //with circular types 84 //so the signature here is simplified 85 as(view: typeof userView): UserView 86 }; 87 recipient: {...}; 88 room: {...}; 89} 90*/ 91 92const userView = view(userModel) 93 .outgoing([userRoomRel]) 94 .incoming([authorRel, recipientRel]); 95type UserView = InferView<typeof userView>; 96 97/* 98type UserView = { 99 id: string; 100 name: string; 101 roomId: string; 102 room: { 103 id: string; 104 ownerId: string; 105 as(view: typeof chatRoomView): ChatRoomView 106 }; 107 readonly inbox: {...}[] & { as(view: typeof messageView): MessageView[] }; 108 readonly outbox: {...}[] & { as(view: typeof messageView): MessageView[] }; 109 */ 110const chatRoomView = view(chatRoomModel) 111 .outgoing([chatRoomOwnerRel]) 112 .incoming([messageRoomRel, userRoomRel]) 113type ChatRoomView = InferView<typeof chatRoomView>; 114 115/* 116type ChatRoomView = { 117 id: string; 118 ownerId: string; 119 owner: {...}; 120 readonly users: {...}[] & { as(view: typeof userView): UserView[]}; 121 readonly messages: {...}[] & { as(view: typeof roomView): MessageView[] }; 122} 123 */ 124 125//combine all your views into a graph schema 126const chatRoomGraphSchema = graphSchema(chatRoomView, [userView, messageView])
1import { OneWayGraph } from 'ts-state-graph/oneWayGraph'; 2 3const chatRoomGraph = new OneWayGraph(chatRoomGraphSchema);
The graph implementation will keep your graph coherent, traversable and reactive. Different implementations will use a different approach to state management and persistence/replication.
1//if using persistence make sure your graph has loaded first then access 2//or create the root 3const chatRoomState = (chatRoomGraph.getRoot() ?? 4 chatRoomGraph.createRoot( 5 { 6 id: 'mainRoom', 7 ownerId: 'alice', 8 }, 9 [ 10 { 11 name: 'User', 12 entity: { 13 id: 'alice', 14 name: 'alice', 15 roomId: 'mainRoom', 16 }, 17 }, 18 ], 19 )); 20 21 22//traverse through entities 23root.owner.as(userView).inbox[0].as(messageView).author.name // -> type string
ts-state-graph can support different graph implementations, where an implementation can control how the type of a view is inferred by exporting
their own source
and target
functions. The example above works with ValtioGraph
.
Different implementations will have a big impact on how your frontend is written, so you can't just swap them out whenever you feel. It's more so that you (or your community) can write a graph implementation for your favourite state management library.
The natural fit is mutable state with observables (or signals or whatever you want to call them). It's probably possible to do it with immutable objects if you resolve references at the point of traversal, and use something like immer to track changes, or calculate diffs separately.
We currently support two full graph implementations. ValtioGraph
and ObservableGraph
(uses legend-state)
The ValtioGraph
implementation uses valtio.
It's attractive because of its simplicity. It can be used as in the example above.
Import source and target from legendState in your graph schema file
1import { source, target } from 'ts-state-graph/valtio'; 2 3///rest of your schema 4...
Instantiate your graph
1import { ValtoGraph } from 'ts-state-graph/valtio'; 2 3export const graph = new ValtioGraph(chatRoomGraphSchema);
Use it!
1const chatRoomState = graph.createRoot( 2 { 3 id: 'mainRoom', 4 ownerId: 'owner', 5 }, 6 [ 7 { 8 name: 'User', 9 entity: { 10 id: 'owner', 11 name: 'owner', 12 roomId: 'mainRoom', 13 }, 14 }, 15 ], 16 ) 17 18 19//the graph is traversed the same as the example above 20chatRoomState.users[0].name = 'fred'
This graph implementation uses legend-state, it has the most potential for high performance, but the DX is not ideal.
This is still in development, the client side graph part works although the api is a little clunky, local persistence works, I'm currently working on remote persistence.
Import source and target from legendState in your graph schema file
1import { source, target } from 'ts-state-graph/legendState'; 2 3///rest of your schema 4...
Instantiate your graph
1import { ObservableGraph, persistGraph } from 'ts-state-graph/legendState'; 2 3export const graph = new ObservableGraph(chatRoomGraphSchema); 4 5export const persistStatus = persistGraph(graph, { 6 databaseName: 'ChatRoomExample23', 7 version: 1, 8}); 9 10//setup persistence 11persistGraph(chatRoomGraph, { 12 databaseName: 'ChatRoomExample23', 13 version: 1, 14});
Use it!
1const chatRoomState = (graph.getRoot() ?? 2 graph.createRoot( 3 { 4 id: 'mainRoom', 5 ownerId: 'owner', 6 }, 7 [ 8 { 9 name: 'User', 10 entity: { 11 id: 'owner', 12 name: 'owner', 13 roomId: 'mainRoom', 14 }, 15 }, 16 ], 17 )) as ObservableObject<ChatRoomView>; 18 19 20//the graph is traversed differently than in the ValtioGraph and OneWayGraph 21chatRoomState.owner.portal(userView).name 22 23chatRoomState.owner.portal(userView).name.onChange(() => {}) //will fire if this user's name changes, but not if the room owner is changed 24chatRoomState.owner.portal(userView).onChange(() => {}) //this will fire if the room owner is changed...
No vulnerabilities found.
No security vulnerabilities found.