Skip to content

Commit c7aa545

Browse files
committed
SY-3833-2: Add Schematic Action Codec and Dispatch Endpoint
Lays the server-side foundation for granular schematic mutations. Oracle codegen: - Adds the 'action' grammar production to the oracle DSL alongside fields, domains, and field omissions inside struct bodies. - Adds resolution.Action and analyzer.collectAction. resolveTypeRefs now walks action payload field types so cross-references inside action bodies resolve correctly to their qualified names. - New oracle/plugin/go/actions plugin generates a discriminated-union codec (Action envelope, Reduce, ReduceAll, NewXxxAction constructors) for any struct that declares actions. Optional fields use the same pointer-prefix logic as gotypes (skip slice / map / msgpack.EncodedJSON). - Formatter learns to emit action blocks so 'oracle fmt' / 'oracle sync' preserves them. Schematic schema and runtime: - schemas/schematic.oracle declares six actions: SetNodePosition, AddNode, RemoveNode, SetEdge, RemoveEdge, SetProps. Hand-written Handle methods live in core/pkg/service/schematic/actions.go alongside the ScopedAction envelope used for cluster broadcast. - Writer.Dispatch applies a sequence of actions atomically inside a single gorp transaction, rejects snapshots, and notifies actionObserver on success. SetData remains in place; no callers are migrated yet. - Service wires actionObserver to a signals translator that publishes scoped action sequences to sy_schematic_set / sy_schematic_delete when cfg.Signals is non-nil. - API exposes Dispatch as a new freighter endpoint (/api/v1/schematic/dispatch) registered alongside the existing SetData endpoint.
1 parent bccda6c commit c7aa545

27 files changed

Lines changed: 2677 additions & 1015 deletions

core/pkg/api/grpc/grpc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ func NewTransport(channelSvc *distchannel.Service) (api.Transport, []grpc.Bindab
106106
a.SchematicRetrieve = fnoop.UnaryServer[schematic.RetrieveRequest, schematic.RetrieveResponse]{}
107107
a.SchematicRename = fnoop.UnaryServer[schematic.RenameRequest, types.Nil]{}
108108
a.SchematicSetData = fnoop.UnaryServer[schematic.SetDataRequest, types.Nil]{}
109+
a.SchematicDispatch = fnoop.UnaryServer[schematic.DispatchRequest, types.Nil]{}
109110
a.SchematicCopy = fnoop.UnaryServer[schematic.CopyRequest, schematic.CopyResponse]{}
110111

111112
// SCHEMATIC SYMBOL

core/pkg/api/http/http.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func NewTransport(router *fhttp.Router, ch *distchannel.Service) api.Transport {
113113
SchematicDelete: fhttp.UnaryServer[schematic.DeleteRequest, types.Nil](router, "/api/v1/schematic/delete"),
114114
SchematicRename: fhttp.UnaryServer[schematic.RenameRequest, types.Nil](router, "/api/v1/schematic/rename"),
115115
SchematicSetData: fhttp.UnaryServer[schematic.SetDataRequest, types.Nil](router, "/api/v1/schematic/set-data"),
116+
SchematicDispatch: fhttp.UnaryServer[schematic.DispatchRequest, types.Nil](router, "/api/v1/schematic/dispatch"),
116117
SchematicCopy: fhttp.UnaryServer[schematic.CopyRequest, schematic.CopyResponse](router, "/api/v1/schematic/copy"),
117118

118119
// SCHEMATIC SYMBOL

core/pkg/api/layer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type Transport struct {
110110
SchematicDelete freighter.UnaryServer[schematic.DeleteRequest, types.Nil]
111111
SchematicRename freighter.UnaryServer[schematic.RenameRequest, types.Nil]
112112
SchematicSetData freighter.UnaryServer[schematic.SetDataRequest, types.Nil]
113+
SchematicDispatch freighter.UnaryServer[schematic.DispatchRequest, types.Nil]
113114
SchematicCopy freighter.UnaryServer[schematic.CopyRequest, schematic.CopyResponse]
114115
// SCHEMATIC SYMBOL
115116
SchematicCreateSymbol freighter.UnaryServer[schematic.CreateSymbolRequest, schematic.CreateSymbolResponse]
@@ -292,6 +293,7 @@ func (l *Layer) BindTo(t Transport) {
292293
t.SchematicDelete,
293294
t.SchematicRename,
294295
t.SchematicSetData,
296+
t.SchematicDispatch,
295297
t.SchematicCopy,
296298

297299
// SCHEMATIC SYMBOL
@@ -438,6 +440,7 @@ func (l *Layer) BindTo(t Transport) {
438440
t.SchematicDelete.BindHandler(l.Schematic.Delete)
439441
t.SchematicRename.BindHandler(l.Schematic.Rename)
440442
t.SchematicSetData.BindHandler(l.Schematic.SetData)
443+
t.SchematicDispatch.BindHandler(l.Schematic.Dispatch)
441444
t.SchematicCopy.BindHandler(l.Schematic.Copy)
442445

443446
// SCHEMATIC SYMBOL

core/pkg/api/schematic/schematic.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,31 @@ func (s *Service) SetData(ctx context.Context, req SetDataRequest) (res types.Ni
110110
})
111111
}
112112

113+
// DispatchRequest carries an action sequence to apply to a single schematic.
114+
// SessionKey identifies the originating client so cluster broadcasts can be
115+
// deduplicated against the local optimistic update.
116+
type DispatchRequest struct {
117+
Key uuid.UUID `json:"key" msgpack:"key"`
118+
SessionKey string `json:"session_key" msgpack:"session_key"`
119+
Actions []schematic.Action `json:"actions" msgpack:"actions"`
120+
}
121+
122+
// Dispatch applies the action sequence to the target schematic atomically.
123+
// Subscribers to the schematic action signals receive the sequence after the
124+
// transaction commits.
125+
func (s *Service) Dispatch(ctx context.Context, req DispatchRequest) (res types.Nil, err error) {
126+
if err = s.access.Enforce(ctx, access.Request{
127+
Subject: auth.GetSubject(ctx),
128+
Action: access.ActionUpdate,
129+
Objects: []ontology.ID{schematic.OntologyID(req.Key)},
130+
}); err != nil {
131+
return res, err
132+
}
133+
return res, s.db.WithTx(ctx, func(tx gorp.Tx) error {
134+
return s.internal.NewWriter(tx).Dispatch(ctx, req.Key, req.SessionKey, req.Actions)
135+
})
136+
}
137+
113138
type (
114139
RetrieveRequest struct {
115140
Keys []uuid.UUID `json:"keys" msgpack:"keys"`

core/pkg/service/schematic/actions.gen.go

Lines changed: 137 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2026 Synnax Labs, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included in the file
4+
// licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with the Business Source
7+
// License, use of this software will be governed by the Apache License, Version 2.0,
8+
// included in the file licenses/APL.txt.
9+
10+
package schematic
11+
12+
import (
13+
"github.com/google/uuid"
14+
"github.com/synnaxlabs/x/encoding/msgpack"
15+
)
16+
17+
// ScopedAction wraps an action sequence with the targeted schematic key and the
18+
// originating client's session key. Subscribers to the action signal channel
19+
// compare SessionKey against their own client key to skip self-originated
20+
// updates (optimistic-UI dedup).
21+
type ScopedAction struct {
22+
Key uuid.UUID `json:"key" msgpack:"key"`
23+
SessionKey string `json:"session_key" msgpack:"session_key"`
24+
Actions []Action `json:"actions" msgpack:"actions"`
25+
}
26+
27+
// Handle moves the named node to the given position. No-op if no node matches.
28+
func (a SetNodePosition) Handle(state Schematic) (Schematic, error) {
29+
for i := range state.Nodes {
30+
if state.Nodes[i].Key == a.Key {
31+
state.Nodes[i].Position = a.Position
32+
break
33+
}
34+
}
35+
return state, nil
36+
}
37+
38+
// Handle appends the node and, if Props is non-nil, seeds the props map under
39+
// the node's key.
40+
func (a AddNode) Handle(state Schematic) (Schematic, error) {
41+
state.Nodes = append(state.Nodes, a.Node)
42+
if a.Props != nil {
43+
if state.Props == nil {
44+
state.Props = make(map[string]msgpack.EncodedJSON)
45+
}
46+
state.Props[a.Node.Key] = a.Props
47+
}
48+
return state, nil
49+
}
50+
51+
// Handle removes the node with the matching key and discards any props entry
52+
// stored under that key.
53+
func (a RemoveNode) Handle(state Schematic) (Schematic, error) {
54+
for i := range state.Nodes {
55+
if state.Nodes[i].Key == a.Key {
56+
state.Nodes = append(state.Nodes[:i], state.Nodes[i+1:]...)
57+
break
58+
}
59+
}
60+
delete(state.Props, a.Key)
61+
return state, nil
62+
}
63+
64+
// Handle inserts the edge if no edge with the same key exists, otherwise
65+
// replaces the existing edge in place.
66+
func (a SetEdge) Handle(state Schematic) (Schematic, error) {
67+
for i := range state.Edges {
68+
if state.Edges[i].Key == a.Edge.Key {
69+
state.Edges[i] = a.Edge
70+
return state, nil
71+
}
72+
}
73+
state.Edges = append(state.Edges, a.Edge)
74+
return state, nil
75+
}
76+
77+
// Handle removes the edge with the matching key. No-op if no edge matches.
78+
func (a RemoveEdge) Handle(state Schematic) (Schematic, error) {
79+
for i := range state.Edges {
80+
if state.Edges[i].Key == a.Key {
81+
state.Edges = append(state.Edges[:i], state.Edges[i+1:]...)
82+
break
83+
}
84+
}
85+
return state, nil
86+
}
87+
88+
// Handle sets the props entry for the given key, replacing any prior value.
89+
func (a SetProps) Handle(state Schematic) (Schematic, error) {
90+
if state.Props == nil {
91+
state.Props = make(map[string]msgpack.EncodedJSON)
92+
}
93+
state.Props[a.Key] = a.Props
94+
return state, nil
95+
}

core/pkg/service/schematic/pb/schematic.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)