Add CultMesh document catalog mutations
This commit is contained in:
parent
cb6301e1cf
commit
52ff632d70
5 changed files with 122 additions and 5 deletions
|
|
@ -1010,6 +1010,68 @@ export class CultMeshDocumentCatalog {
|
|||
return this.document(schema, options).latest(context);
|
||||
}
|
||||
|
||||
public canReplace(schema: CultMeshDocumentSchemaDescriptor): boolean {
|
||||
return this.tryDocument(schema)?.canReplace ?? false;
|
||||
}
|
||||
|
||||
public replace<TDocument>(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
value: TDocument,
|
||||
options?: { parse?: (value: unknown) => TDocument; context?: CultMeshQueryContext | string },
|
||||
): Promise<void>;
|
||||
public replace<TDocument>(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
context: CultMeshQueryContext | string,
|
||||
value: TDocument,
|
||||
options?: { parse?: (value: unknown) => TDocument },
|
||||
): Promise<void>;
|
||||
public replace<TDocument>(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
contextOrValue: CultMeshQueryContext | string | TDocument,
|
||||
valueOrOptions?: TDocument | { parse?: (value: unknown) => TDocument; context?: CultMeshQueryContext | string },
|
||||
maybeOptions: { parse?: (value: unknown) => TDocument } = {},
|
||||
): Promise<void> {
|
||||
const hasContext =
|
||||
typeof contextOrValue === "string" || isCultMeshQueryContext(contextOrValue);
|
||||
const context = hasContext
|
||||
? contextOrValue as CultMeshQueryContext | string
|
||||
: (valueOrOptions as { context?: CultMeshQueryContext | string } | undefined)?.context ?? "local";
|
||||
const value = hasContext ? valueOrOptions as TDocument : contextOrValue as TDocument;
|
||||
const options = hasContext ? maybeOptions : valueOrOptions as { parse?: (value: unknown) => TDocument } | undefined;
|
||||
return this.document(schema, options).replace(context, value);
|
||||
}
|
||||
|
||||
public canSubmitPrediction(schema: CultMeshDocumentSchemaDescriptor): boolean {
|
||||
return this.tryDocument(schema)?.canSubmitPrediction ?? false;
|
||||
}
|
||||
|
||||
public submitPrediction<TDocument>(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
value: TDocument,
|
||||
options?: { parse?: (value: unknown) => TDocument; context?: CultMeshQueryContext | string },
|
||||
): Promise<void>;
|
||||
public submitPrediction<TDocument>(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
context: CultMeshQueryContext | string,
|
||||
value: TDocument,
|
||||
options?: { parse?: (value: unknown) => TDocument },
|
||||
): Promise<void>;
|
||||
public submitPrediction<TDocument>(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
contextOrValue: CultMeshQueryContext | string | TDocument,
|
||||
valueOrOptions?: TDocument | { parse?: (value: unknown) => TDocument; context?: CultMeshQueryContext | string },
|
||||
maybeOptions: { parse?: (value: unknown) => TDocument } = {},
|
||||
): Promise<void> {
|
||||
const hasContext =
|
||||
typeof contextOrValue === "string" || isCultMeshQueryContext(contextOrValue);
|
||||
const context = hasContext
|
||||
? contextOrValue as CultMeshQueryContext | string
|
||||
: (valueOrOptions as { context?: CultMeshQueryContext | string } | undefined)?.context ?? "local";
|
||||
const value = hasContext ? valueOrOptions as TDocument : contextOrValue as TDocument;
|
||||
const options = hasContext ? maybeOptions : valueOrOptions as { parse?: (value: unknown) => TDocument } | undefined;
|
||||
return this.document(schema, options).submitPrediction(context, value);
|
||||
}
|
||||
|
||||
public watch<TDocument>(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
callback: (value: TDocument) => void,
|
||||
|
|
|
|||
|
|
@ -104,11 +104,19 @@ test("CultMesh TS document handles hide local cache plumbing behind typed reacti
|
|||
assert.equal((await alias.latest()).body, "updated");
|
||||
|
||||
const catalog = CultMesh.documents(document);
|
||||
assert.equal(catalog.canReplace(noteAliasDocument), true);
|
||||
await catalog.replace(noteAliasDocument, {
|
||||
noteId: "note:1",
|
||||
body: "catalog-updated",
|
||||
}, {
|
||||
parse: value => noteAliasDocument.schema.parse(value),
|
||||
context: "browser-client",
|
||||
});
|
||||
assert.equal(
|
||||
(await catalog.latest(noteAliasDocument, "browser-client", {
|
||||
parse: value => noteAliasDocument.schema.parse(value),
|
||||
})).body,
|
||||
"updated",
|
||||
"catalog-updated",
|
||||
);
|
||||
assert.throws(
|
||||
() => document.asSchemaAlias({ schemaId: "cultmesh.other.v0" }),
|
||||
|
|
@ -138,14 +146,18 @@ test("CultMesh TS document handles submit predictions through configured authori
|
|||
);
|
||||
const verse = CultMesh.verse("starbridge", "pilot-a");
|
||||
const bound = document.bind(verse);
|
||||
const catalog = CultMesh.documents(document);
|
||||
|
||||
assert.equal(document.canReplace, false);
|
||||
assert.equal(document.canSubmitPrediction, true);
|
||||
assert.equal(bound.canSubmitPrediction, true);
|
||||
assert.equal(catalog.canSubmitPrediction(noteAliasDocument), true);
|
||||
|
||||
await bound.submitPrediction({
|
||||
await catalog.submitPrediction(noteAliasDocument, "pilot-a", {
|
||||
noteId: "note:prediction",
|
||||
body: "predicted",
|
||||
}, {
|
||||
parse: value => noteAliasDocument.schema.parse(value),
|
||||
});
|
||||
|
||||
assert.deepEqual(contexts, ["pilot-a:network"]);
|
||||
|
|
|
|||
|
|
@ -1177,6 +1177,34 @@ namespace GameCult.Mesh
|
|||
return Document<TDocument>().Latest();
|
||||
}
|
||||
|
||||
/// <summary>Gets whether one typed document can be replaced by CLR type or same-schema alias.</summary>
|
||||
public bool CanReplace<TDocument>()
|
||||
where TDocument : class
|
||||
{
|
||||
return TryGetDocument<TDocument>(out var document) && document.CanReplace;
|
||||
}
|
||||
|
||||
/// <summary>Replaces one typed document by CLR type or same-schema alias.</summary>
|
||||
public Task ReplaceAsync<TDocument>(TDocument value)
|
||||
where TDocument : class
|
||||
{
|
||||
return Document<TDocument>().ReplaceAsync(value);
|
||||
}
|
||||
|
||||
/// <summary>Gets whether one typed document can submit client predictions by CLR type or same-schema alias.</summary>
|
||||
public bool CanSubmitPrediction<TDocument>()
|
||||
where TDocument : class
|
||||
{
|
||||
return TryGetDocument<TDocument>(out var document) && document.CanSubmitPrediction;
|
||||
}
|
||||
|
||||
/// <summary>Submits a client prediction for one typed document by CLR type or same-schema alias.</summary>
|
||||
public Task SubmitPredictionAsync<TDocument>(TDocument value)
|
||||
where TDocument : class
|
||||
{
|
||||
return Document<TDocument>().SubmitPredictionAsync(value);
|
||||
}
|
||||
|
||||
/// <summary>Watches one typed document by CLR type or same-schema alias.</summary>
|
||||
public Observable<TDocument> Watch<TDocument>()
|
||||
where TDocument : class
|
||||
|
|
|
|||
|
|
@ -178,7 +178,10 @@ portable machinery that makes it feel native.
|
|||
schema version, and resolves same-schema CLR aliases by delegating to
|
||||
`CultMeshDocumentHandle<TDocument>.AsSchemaAlias<TAlias>()`. Domain facades
|
||||
can expose `Document<T>()`, `DocumentBySchema(...)`, `LatestAsync<T>()`, and
|
||||
`Watch<T>()` without owning schema dictionaries or alias serializers.
|
||||
`Watch<T>()` without owning schema dictionaries or alias serializers. Mutable
|
||||
facades can also call `ReplaceAsync<T>(...)` or
|
||||
`SubmitPredictionAsync<T>(...)` directly on the catalog when they only need
|
||||
the schema-aliased operation and not the intermediate handle.
|
||||
- `CultMeshCollectionHandle<TDocument>` is the typed reactive edge for
|
||||
multi-record state. It exposes one `LatestAsync()` collection snapshot and a
|
||||
`WatchChanges()` stream while hiding whether the backing source is an
|
||||
|
|
|
|||
|
|
@ -775,11 +775,21 @@ public sealed class CultMeshStreamingTests
|
|||
observed.Text.Should().Be("cache-replaced");
|
||||
observed.Revision.Should().Be(2);
|
||||
|
||||
var catalog = CultMesh.Documents(handle);
|
||||
catalog.CanReplace<MeshNoteAliasDocument>().Should().BeTrue();
|
||||
await catalog.ReplaceAsync(new MeshNoteAliasDocument
|
||||
{
|
||||
Schema = "tests.mesh_note.v1",
|
||||
Text = "cache-catalog-replaced",
|
||||
Revision = 3
|
||||
});
|
||||
cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("cache-catalog-replaced");
|
||||
|
||||
Func<Task> act = () => alias.SubmitPredictionAsync(new MeshNoteAliasDocument
|
||||
{
|
||||
Schema = "tests.mesh_note.v1",
|
||||
Text = "cache-predicted",
|
||||
Revision = 3
|
||||
Revision = 4
|
||||
});
|
||||
await act.Should().ThrowAsync<NotSupportedException>();
|
||||
}
|
||||
|
|
@ -820,10 +830,12 @@ public sealed class CultMeshStreamingTests
|
|||
|
||||
handle.CanReplace.Should().BeTrue();
|
||||
alias.CanSubmitPrediction.Should().BeTrue();
|
||||
var catalog = CultMesh.Documents(handle);
|
||||
catalog.CanSubmitPrediction<MeshNoteAliasDocument>().Should().BeTrue();
|
||||
|
||||
MeshNoteDocument observed = null!;
|
||||
using var subscription = handle.Watch(value => observed = value);
|
||||
await alias.SubmitPredictionAsync(new MeshNoteAliasDocument
|
||||
await catalog.SubmitPredictionAsync(new MeshNoteAliasDocument
|
||||
{
|
||||
Schema = "tests.mesh_note.v1",
|
||||
Text = "predicted-thermal",
|
||||
|
|
|
|||
Loading…
Reference in a new issue