Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion backend/FwLite/FwLiteShared/Events/JsEventListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ public JsEventListener(ILogger<JsEventListener> logger, GlobalEventBus globalEve
}

[JSInvokable]
[TsFunction(Type = "Promise<IFwEvent | null>")]
public ValueTask<IFwEvent?> LastEvent(FwEventType type)
{
return ValueTask.FromResult(_globalEventBus.GetLastEvent(type));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ namespace FwLiteShared.Services;
public interface IPreferencesService
{
[JSInvokable]
[TsFunction(Type = "Promise<string | null>")]
string? Get(string key);
[JSInvokable]
void Set(string key, string value);
Expand Down
11 changes: 1 addition & 10 deletions backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,12 @@ public ValueTask<CustomView[]> GetCustomViews()
}

[JSInvokable]
[TsFunction(Type = "Promise<ICustomView | null>")]
public Task<CustomView?> GetCustomView(Guid id)
{
return _wrappedApi.GetCustomView(id);
}

[JSInvokable]
[TsFunction(Type = "Promise<IComplexFormType | null>")]
public Task<ComplexFormType?> GetComplexFormType(Guid id)
{
return _wrappedApi.GetComplexFormType(id);
Expand Down Expand Up @@ -139,35 +137,30 @@ public Task<Entry[]> SearchEntries(string query, QueryOptions? options = null)
}

[JSInvokable]
[TsFunction(Type = "Promise<IEntry | null>")]
public Task<Entry?> GetEntry(Guid id)
{
return Task.Run(async () => await _wrappedApi.GetEntry(id));
}

[JSInvokable]
[TsFunction(Type = "Promise<ISense | null>")]
public Task<Sense?> GetSense(Guid entryId, Guid id)
{
return _wrappedApi.GetSense(entryId, id);
}

[JSInvokable]
[TsFunction(Type = "Promise<IPartOfSpeech | null>")]
public Task<PartOfSpeech?> GetPartOfSpeech(Guid id)
{
return _wrappedApi.GetPartOfSpeech(id);
}

[JSInvokable]
[TsFunction(Type = "Promise<ISemanticDomain | null>")]
public Task<SemanticDomain?> GetSemanticDomain(Guid id)
{
return _wrappedApi.GetSemanticDomain(id);
}

[JSInvokable]
[TsFunction(Type = "Promise<IExampleSentence | null>")]
public Task<ExampleSentence?> GetExampleSentence(Guid entryId, Guid senseId, Guid id)
{
return _wrappedApi.GetExampleSentence(entryId, senseId, id);
Expand Down Expand Up @@ -286,7 +279,6 @@ public ValueTask<CommentThread[]> GetCommentThreads(SubjectType subjectType, Gui
}

[JSInvokable]
[TsFunction(Type = "Promise<ICommentThread | null>")]
public Task<CommentThread?> GetCommentThread(Guid id)
{
return _wrappedApi.GetCommentThread(id);
Expand All @@ -299,7 +291,6 @@ public ValueTask<UserComment[]> GetUserComments(Guid threadId)
}

[JSInvokable]
[TsFunction(Type = "Promise<IUserComment | null>")]
public Task<UserComment?> GetUserComment(Guid id)
{
return _wrappedApi.GetUserComment(id);
Expand Down Expand Up @@ -531,7 +522,7 @@ public async Task DeletePicture(Guid entryId, Guid senseId, Guid pictureId)
}

[JSInvokable]
public async Task<ReadFileResponseJs?> GetFileStream(string mediaUri, bool downloadIfMissing)
public async Task<ReadFileResponseJs> GetFileStream(string mediaUri, bool downloadIfMissing)
{
var result = await _wrappedApi.GetFileStream(new MediaUri(mediaUri), downloadIfMissing);
var stream = result.Stream is null ? null : new DotNetStreamReference(result.Stream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ public async Task DisposeService(DotNetObjectReference<IAsyncDisposable> service
}

[JSInvokable]
public Task<string?> TryGetCrdtProjectName(string code)
public string? TryGetCrdtProjectName(string code)
{
var crdtProject = crdtProjectsService.GetProject(code);
return Task.FromResult(crdtProject?.Data?.Name);
return crdtProject?.Data?.Name;
}

[JSInvokable]
Expand Down
1 change: 0 additions & 1 deletion backend/FwLite/FwLiteShared/Services/UpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace FwLiteShared.Services;
public class UpdateService(UpdateChecker updateChecker)
{
[JSInvokable]
[TsFunction(Type = "Promise<IAvailableUpdate | null>")]
public Task<AvailableUpdate?> CheckForUpdates()
{
return Task.Run(async () => await updateChecker.CheckForUpdate());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using MiniLcm.Media;
using MediaFile = MiniLcm.Media.MediaFile;
using Microsoft.Extensions.Logging;
using Reinforced.Typings.Generators;
using SIL.Harmony.Changes;
using SIL.Harmony.Resource;

Expand Down Expand Up @@ -228,6 +229,7 @@ private static void ConfigureFwLiteSharedTypes(ConfigurationBuilder builder)

private static MethodExportBuilder AlwaysReturnPromise(this MethodExportBuilder exportBuilder)
{
exportBuilder.WithCodeGenerator<NullableMethodReturnBuilder>();
var isUpdatePatchMethod = exportBuilder.Member.GetParameters()
.Any(p => p.ParameterType.IsGenericType &&
p.ParameterType.GetGenericTypeDefinition() == typeof(UpdateObjectInput<>));
Expand Down Expand Up @@ -310,4 +312,40 @@ public override void Visit(RtImport node)
base.Visit(node);
}
}

/// <summary>
/// introspects the return type of a method to determine if it is nullable and modifies the return type to be T | undefined
/// before Task<T?> -> Promise<T> after Promise<T | undefined>
/// </summary>
internal class NullableMethodReturnBuilder : MethodCodeGenerator
{
private static NullabilityInfoContext _nullabilityInfoContext = new();
protected override void GetFunctionNameAndReturnType(MethodInfo element, TypeResolver resolver, out string name, out RtTypeName type)
{
base.GetFunctionNameAndReturnType(element, resolver, out name, out type);
if (type is RtAsyncType {TypeNameOfAsync: RtSimpleTypeName asyncResolveType})
{
var nullabilityInfo = _nullabilityInfoContext.Create(element.ReturnParameter);
var mayBeNull = false;
//some methods return Task<T> but some just return T and we force them to always show as Promise<T>
//but here we need to know which as we're looking at the underlying type
if (element.ReturnType.IsGenericType &&
(element.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)
|| element.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))
{
mayBeNull = (nullabilityInfo.GenericTypeArguments.FirstOrDefault()?.ReadState ?? NullabilityState.Nullable) == NullabilityState.Nullable;
}
else
{
mayBeNull = nullabilityInfo.ReadState == NullabilityState.Nullable;
}

if (mayBeNull)
{
type = new RtAsyncType(new RtSimpleTypeName(asyncResolveType.TypeName + " | undefined"));
}
}

}
}
}
3 changes: 2 additions & 1 deletion frontend/viewer/src/home/ServersList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@

async function refreshServerProjects(server: ILexboxServer, force: boolean = false) {
loadingServerProjects = server.id;
remoteProjects[server.id] = await projectsService.serverProjects(server.id, force);
const projects = await projectsService.serverProjects(server.id, force);
if (projects) remoteProjects[server.id] = projects;
remoteProjects = remoteProjects;
loadingServerProjects = undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {FwEventType} from './FwEventType';

export interface IJsEventListener
{
nextEventAsync() : Promise<IFwEvent>;
lastEvent(type: FwEventType) : Promise<IFwEvent | null>;
nextEventAsync() : Promise<IFwEvent | undefined>;
lastEvent(type: FwEventType) : Promise<IFwEvent | undefined>;
}
/* eslint-enable */
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface ICombinedProjectsService
{
supportsFwData() : Promise<boolean>;
remoteProjects() : Promise<IServerProjects[]>;
serverProjects(serverId: string, forceRefresh: boolean) : Promise<IServerProjects>;
serverProjects(serverId: string, forceRefresh: boolean) : Promise<IServerProjects | undefined>;
localProjects() : Promise<IProjectModel[]>;
downloadProjectByCode(code: string, server: ILexboxServer, userRole?: UserProjectRole) : Promise<DownloadProjectByCodeResult>;
downloadProject(project: IProjectModel) : Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface IHistoryServiceJsInvokable
projectActivity(skip: number, take: number, authorFilterKeys?: string[], changeTypeKeys?: string[], sort?: ActivitySort) : Promise<IProjectActivity[]>;
listActivityAuthors() : Promise<IActivityAuthor[]>;
listActivityChangeTypes() : Promise<IActivityChangeType[]>;
getSnapshot(snapshotId: string) : Promise<IObjectSnapshot>;
getSnapshot(snapshotId: string) : Promise<IObjectSnapshot | undefined>;
getHistory(entityId: string) : Promise<IHistoryLineItem[]>;
loadChangeContext(commitId: string, changeIndex: number) : Promise<IChangeContext>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type {IFilterQueryOptions} from '../../MiniLcm/IFilterQueryOptions';
import type {IIndexQueryOptions} from '../../MiniLcm/IIndexQueryOptions';
import type {IEntry} from '../../MiniLcm/Models/IEntry';
import type {IQueryOptions} from '../../MiniLcm/IQueryOptions';
import type {ISense} from '../../MiniLcm/Models/ISense';
import type {IExampleSentence} from '../../MiniLcm/Models/IExampleSentence';
import type {IWritingSystem} from '../../MiniLcm/Models/IWritingSystem';
import type {WritingSystemType} from '../../MiniLcm/Models/WritingSystemType';
import type {ICommentThread} from '../../MiniLcm/Models/ICommentThread';
Expand All @@ -23,8 +25,6 @@ import type {IUserComment} from '../../MiniLcm/Models/IUserComment';
import type {ThreadStatus} from '../../MiniLcm/Models/ThreadStatus';
import type {ICreateEntryOptions} from '../../MiniLcm/ICreateEntryOptions';
import type {IComplexFormComponent} from '../../MiniLcm/Models/IComplexFormComponent';
import type {ISense} from '../../MiniLcm/Models/ISense';
import type {IExampleSentence} from '../../MiniLcm/Models/IExampleSentence';
import type {IPicture} from '../../MiniLcm/Models/IPicture';
import type {IReadFileResponseJs} from './IReadFileResponseJs';
import type {IUploadFileResponse} from '../../MiniLcm/Media/IUploadFileResponse';
Expand All @@ -39,18 +39,18 @@ export interface IMiniLcmJsInvokable
getSemanticDomains() : Promise<ISemanticDomain[]>;
getComplexFormTypes() : Promise<IComplexFormType[]>;
getCustomViews() : Promise<ICustomView[]>;
getCustomView(id: string) : Promise<ICustomView | null>;
getComplexFormType(id: string) : Promise<IComplexFormType | null>;
getCustomView(id: string) : Promise<ICustomView | undefined>;
getComplexFormType(id: string) : Promise<IComplexFormType | undefined>;
getMorphTypes() : Promise<IMorphType[]>;
countEntries(query?: string, options?: IFilterQueryOptions) : Promise<number>;
getEntryIndex(id: string, query?: string, options?: IIndexQueryOptions) : Promise<number>;
getEntries(options?: IQueryOptions) : Promise<IEntry[]>;
searchEntries(query: string, options?: IQueryOptions) : Promise<IEntry[]>;
getEntry(id: string) : Promise<IEntry | null>;
getSense(entryId: string, id: string) : Promise<ISense | null>;
getPartOfSpeech(id: string) : Promise<IPartOfSpeech | null>;
getSemanticDomain(id: string) : Promise<ISemanticDomain | null>;
getExampleSentence(entryId: string, senseId: string, id: string) : Promise<IExampleSentence | null>;
getEntry(id: string) : Promise<IEntry | undefined>;
getSense(entryId: string, id: string) : Promise<ISense | undefined>;
getPartOfSpeech(id: string) : Promise<IPartOfSpeech | undefined>;
getSemanticDomain(id: string) : Promise<ISemanticDomain | undefined>;
getExampleSentence(entryId: string, senseId: string, id: string) : Promise<IExampleSentence | undefined>;
createWritingSystem(type: WritingSystemType, writingSystem: IWritingSystem) : Promise<IWritingSystem>;
updateWritingSystem(before: IWritingSystem, after: IWritingSystem) : Promise<IWritingSystem>;
createPartOfSpeech(partOfSpeech: IPartOfSpeech) : Promise<IPartOfSpeech>;
Expand All @@ -66,9 +66,9 @@ export interface IMiniLcmJsInvokable
updateCustomView(customView: ICustomView) : Promise<ICustomView>;
deleteCustomView(id: string) : Promise<void>;
getCommentThreads(subjectType: SubjectType, subjectId: string, includeComments?: boolean) : Promise<ICommentThread[]>;
getCommentThread(id: string) : Promise<ICommentThread | null>;
getCommentThread(id: string) : Promise<ICommentThread | undefined>;
getUserComments(threadId: string) : Promise<IUserComment[]>;
getUserComment(id: string) : Promise<IUserComment | null>;
getUserComment(id: string) : Promise<IUserComment | undefined>;
getUnreadComments(threadId?: string) : Promise<IUserComment[]>;
getUnreadCommentsForSubject(subjectType: SubjectType, subjectId: string) : Promise<IUserComment[]>;
countUnreadComments(threadId?: string) : Promise<number>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

export interface IPreferencesService
{
get(key: string) : Promise<string | null>;
get(key: string) : Promise<string | undefined>;
set(key: string, value: string) : Promise<void>;
remove(key: string) : Promise<void>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {IProjectScope} from './IProjectScope';
export interface IProjectServicesProvider extends IAsyncDisposable
{
disposeService(service: DotNet.DotNetObject) : Promise<void>;
tryGetCrdtProjectName(code: string) : Promise<string>;
tryGetCrdtProjectName(code: string) : Promise<string | undefined>;
openCrdtProject(code: string) : Promise<IProjectScope>;
openFwDataProject(projectName: string) : Promise<IProjectScope>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export interface ISyncServiceJsInvokable
{
getSyncStatus() : Promise<IProjectSyncStatus>;
triggerFwHeadlessSync() : Promise<ISyncJobResult>;
countPendingCrdtCommits() : Promise<IPendingCommits>;
getLatestSyncedCommitDate() : Promise<string>;
countPendingCrdtCommits() : Promise<IPendingCommits | undefined>;
getLatestSyncedCommitDate() : Promise<string | undefined>;
executeSync(skipNotifications: boolean) : Promise<ISyncResults>;
getCurrentServer() : Promise<ILexboxServer>;
getCurrentServer() : Promise<ILexboxServer | undefined>;
}
/* eslint-enable */
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.

import type {UpdateResult} from '../AppUpdate/UpdateResult';
import type {IAvailableUpdate} from '../AppUpdate/IAvailableUpdate';
import type {UpdateResult} from '../AppUpdate/UpdateResult';

export interface IUpdateService
{
checkForUpdates() : Promise<IAvailableUpdate | null>;
checkForUpdates() : Promise<IAvailableUpdate | undefined>;
applyUpdate(update: IAvailableUpdate) : Promise<UpdateResult>;
}
/* eslint-enable */
8 changes: 4 additions & 4 deletions frontend/viewer/src/lib/services/browser-app-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type {IJsEventListener} from '$lib/dotnet-types/generated-types/FwLiteSha
import type {IPreferencesService} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IPreferencesService';

const localStoragePreferencesService: IPreferencesService = {
get(key: string): Promise<string | null> {
return Promise.resolve(localStorage.getItem(key));
get(key: string): Promise<string | undefined> {
return Promise.resolve(localStorage.getItem(key) ?? undefined);
},
set(key: string, value: string): Promise<void> {
localStorage.setItem(key, value);
Expand All @@ -22,8 +22,8 @@ const noopJsEventListener: IJsEventListener = {
// Never resolves — no events to deliver in a browser-only context
return new Promise<IFwEvent>(() => {});
},
lastEvent(): Promise<IFwEvent | null> {
return Promise.resolve(null);
lastEvent(): Promise<IFwEvent | undefined> {
return Promise.resolve(undefined);
},
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/viewer/src/lib/services/event-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class EventBus {
}

private async eventLoop(jsEventListener: IJsEventListener) {
let event: IFwEvent;
let event: IFwEvent | undefined;
while (true) {
event = await jsEventListener.nextEventAsync();
if (!event) return;
Expand Down
3 changes: 2 additions & 1 deletion frontend/viewer/src/lib/services/service-provider-dotnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export function wrapInProxy<K extends ServiceKey>(dotnetObject: DotNet.DotNetObj
console.debug(`[Dotnet Proxy] Calling ${serviceName} method ${dotnetMethodName}`, args);
args = transformArgs(args);
try {
const result = await target.invokeMethodAsync(dotnetMethodName, ...args);
let result = await target.invokeMethodAsync(dotnetMethodName, ...args);
if (result === null) result = undefined;//ensure that if the return is null rewrite it to undefined to match the contract
console.debug(`[Dotnet Proxy] ${serviceName} method ${dotnetMethodName} returned`, result);
return result;
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/viewer/src/lib/updates/UpdateDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
const config = useFwLiteConfig();
const updateService = useUpdateService();

let checkPromise = $state<Promise<IAvailableUpdate | null>>();
let checkPromise = $state<Promise<IAvailableUpdate | undefined>>();
let installPromise = $state<Promise<UpdateResult>>();

const eventBus = useEventBus();
Expand Down
2 changes: 1 addition & 1 deletion frontend/viewer/src/lib/updates/UpdateDialogContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import {getReleaseUrl} from './utils';

type Props = {
checkPromise?: Promise<IAvailableUpdate | null>;
checkPromise?: Promise<IAvailableUpdate | undefined>;
installPromise?: Promise<UpdateResult>;
installUpdate: (update: IAvailableUpdate) => Promise<void>;
installProgress?: number;
Expand Down
4 changes: 2 additions & 2 deletions frontend/viewer/src/project/browse/EntryView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
},
);

function snapshotEntry(entry: IEntry | null): IEntry | null {
function snapshotEntry(entry: IEntry | undefined): IEntry | undefined {
// IMMEDIATELY take a snapshot to ensure it doesn't get mutated by the editor before EntryPersistence gets it.
// (dirty fields immediately push their current dirty value into the entry object, which can corrupt the update diff.)
latestPersistedSnapshot = entry ? Object.freeze(copy(entry)) : undefined;
Expand All @@ -76,7 +76,7 @@

// For entry updates that arrive OUTSIDE the resource fetcher (event bus, restore), we must
// push the new value into the resource ourselves via mutate().
function setEntry(entry: IEntry | null): IEntry | null {
function setEntry(entry: IEntry | undefined): IEntry | undefined {
snapshotEntry(entry);
entryResource.mutate(entry);
return entry;
Expand Down
Loading
Loading