File size: 5,154 Bytes
bc575bc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | import {
type Context,
createRemoteServiceEndpoint,
type JsonValue,
RemoteServiceProvider,
replicatedState,
} from "@earendil-works/chord";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import type { RoutedServerServiceAttachment, RoutedServerServiceHost } from "@earendil-works/pi-server";
import { PresentationPlugins } from "./plugins.ts";
import {
type SessionCreateOptions,
SessionDirectory,
type SessionDirectoryState,
SessionManagement,
type SessionSummary,
} from "./sessions.ts";
export interface ExperimentalServerServices {
readonly host: RoutedServerServiceHost;
refresh(context?: Context): Promise<void>;
dispose(): Promise<void>;
}
export async function createExperimentalServerServices(options: {
list(context: Context): Promise<SessionSummary[]>;
create(createOptions: SessionCreateOptions, context: Context): Promise<SessionSummary>;
remove(sessionId: string, context: Context): Promise<void>;
prepareSessionPlugins(
sessionId: string,
packagePaths: readonly string[] | undefined,
context: Context,
): Promise<{ readonly packagePaths: readonly string[]; readonly presentationPlugins: JsonValue }>;
reloadPresentationPlugins(packagePaths: readonly string[], context: Context): Promise<JsonValue>;
}): Promise<ExperimentalServerServices> {
let revision = 1;
const directory = replicatedState<SessionDirectoryState>({
revision,
sessions: await options.list(BACKGROUND_CONTEXT),
});
const attachments = new Set<RoutedServerServiceAttachment>();
let mutationTail = Promise.resolve();
const refreshNow = async (context: Context): Promise<void> => {
const sessions = await options.list(context);
revision += 1;
directory.state.revision = revision;
directory.state.sessions = sessions;
directory.publish(context);
};
const serialize = <T>(operation: () => Promise<T>): Promise<T> => {
const result = mutationTail.catch(() => {}).then(operation);
mutationTail = result.then(
() => undefined,
() => undefined,
);
return result;
};
return {
host: {
attachClient(presentation) {
let preparedPluginPackagePaths: readonly string[] | undefined;
const provider = new RemoteServiceProvider([
{ service: SessionDirectory, mode: "singleton" },
{ service: SessionManagement, mode: "singleton" },
{ service: PresentationPlugins, mode: "singleton" },
]);
provider.provide(SessionDirectory, { state: directory });
provider.provide(PresentationPlugins, {
prepareSession: ({ sessionId, packagePaths }, context) =>
serialize(async () => {
const selected = await options.prepareSessionPlugins(
sessionId,
packagePaths ?? undefined,
context,
);
preparedPluginPackagePaths = selected.packagePaths;
return selected.presentationPlugins;
}),
reload: (context) =>
serialize(() => {
if (preparedPluginPackagePaths === undefined) {
throw new Error("No Session plugin selection is prepared");
}
return options.reloadPresentationPlugins(preparedPluginPackagePaths, context);
}),
});
provider.provide(SessionManagement, {
create: (createOptions, context) =>
serialize(async () => {
const created = await options.create(createOptions, context);
await refreshNow(context);
return created;
}),
remove: (sessionId, context) =>
serialize(async () => {
await presentation.prepareSessionRemoval(sessionId, context);
await options.remove(sessionId, context);
await refreshNow(context);
}),
attach: (sessionId, context) =>
serialize(async () => {
await presentation.attachSession(sessionId, context);
}),
detach: (context) =>
serialize(async () => {
await presentation.detachSession(context);
preparedPluginPackagePaths = undefined;
}),
});
const attachment = createProviderAttachment(provider, () => attachments.delete(attachment));
attachments.add(attachment);
return attachment;
},
},
refresh: (context = BACKGROUND_CONTEXT) => serialize(() => refreshNow(context)),
async dispose() {
const releases = await Promise.allSettled(
[...attachments].map((attachment) => attachment.release(BACKGROUND_CONTEXT)),
);
attachments.clear();
await mutationTail;
const errors = releases.flatMap((result) => (result.status === "rejected" ? [result.reason] : []));
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Failed to release server service attachments");
},
};
}
function createProviderAttachment(
provider: RemoteServiceProvider,
onRelease: () => void,
): RoutedServerServiceAttachment {
const endpoint = createRemoteServiceEndpoint(provider);
let released = false;
return {
invokeService(call, publish, context) {
if (released) return Promise.reject(new Error("Server service attachment is released"));
return endpoint.invoke(call, publish, context);
},
release() {
if (released) return;
released = true;
endpoint.dispose();
provider.dispose();
onRelease();
},
};
}
|