blob: 173f7b43b4b73fb99ec5dbc6578185f58ababc45 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package sessionflow
import (
"crypto/sha256"
"encoding/hex"
"sort"
"strconv"
"strings"
)
// Entity ids.
//
// An id must be derivable from evidence that does not move. Position is the
// tempting choice and the wrong one: a node keyed by "the 14th record in the
// stream" gets a different key the moment an earlier source is backfilled, and
// a changed key cannot supersede its own earlier revision - the fold would hold
// both, one stale and one current, with no way to tell them apart.
//
// So ids are built from the runtime's own identity for a thing, scoped by
// session and stream. Where the runtime supplies no identity, the id is derived
// from the landed location of the evidence, which is itself immutable once
// written.
// NodeID builds a node id from stable parts.
//
// Parts are joined rather than hashed so an id stays legible in a round file
// and in an error message. Legibility matters more than length here: these are
// read by people debugging why two revisions did not merge.
func NodeID(kind string, parts ...string) string {
return join(append([]string{kind}, parts...))
}
// RelationID builds a relation id.
//
// A relation is keyed by its endpoints and type, not by the evidence that
// revealed it. Two records asserting the same edge are the same relation
// observed twice, and must fold to one - keying by evidence would keep both.
func RelationID(typ, from, to string) string {
return join([]string{"rel", typ, from, to})
}
// UnresolvedID builds an unresolved-entry id.
//
// Keyed by what is missing, so the later revision that resolves it lands on the
// same key. Keyed by the round that noticed, every round would open a new one.
func UnresolvedID(kind, ref string) string {
return join([]string{"unres", kind, ref})
}
// RefID names a landed location, for use where the runtime gives no identity.
func RefID(kind string, r Ref) string {
s := join([]string{kind, strconv.FormatUint(r.Seq, 10), strconv.FormatUint(r.Row, 10)})
if r.Block != nil {
s += ":" + strconv.Itoa(*r.Block)
}
return s
}
func join(parts []string) string {
clean := make([]string, 0, len(parts))
for _, p := range parts {
if p == "" {
continue
}
clean = append(clean, strings.ReplaceAll(p, "/", "_"))
}
return strings.Join(clean, "/")
}
// ChainInputDigest extends an input digest with newly consumed evidence.
//
// It is a chain, not a recomputation over everything read so far:
//
// digestₙ = H(digestₙ₋₁ || sorted digests of the evidence round n consumed)
//
// Recomputing over the full history would make each round cost proportional to
// the whole conversation, so a long-running one would slow down without bound -
// exactly the property an incremental design exists to avoid. Chaining keeps
// each round proportional to what is new, and still binds every round
// transitively to all evidence before it.
//
// Inputs are sorted so that discovery order - which depends on directory
// iteration and is not stable - cannot change the digest.
func ChainInputDigest(previous string, added []string) string {
sorted := append([]string(nil), added...)
sort.Strings(sorted)
h := sha256.New()
h.Write([]byte(previous))
for _, a := range sorted {
h.Write([]byte{0})
h.Write([]byte(a))
}
return hex.EncodeToString(h.Sum(nil))
}