Skip to content

Commit

Permalink
Merge pull request #135 from zetkin/issue-25/merge-store-data
Browse files Browse the repository at this point in the history
Refreshing store data
  • Loading branch information
WULCAN authored Oct 19, 2024
2 parents 2328532 + 92644b6 commit 4e3bccb
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 40 deletions.
2 changes: 2 additions & 0 deletions webapp/src/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RepoGit } from '@/RepoGit';
import { ServerConfig } from '@/utils/serverConfig';
import { Store } from '@/store/Store';
import YamlTranslationAdapter from '@/utils/adapters/YamlTranslationAdapter';
import MessageAdapterFactory from './utils/adapters/MessageAdapterFactory';

export class Cache {
public static async getLanguage(projectName: string, lang: string) {
Expand All @@ -33,6 +34,7 @@ export class Cache {

if (!globalThis.store.hasProjectStore(lyraProjectConfig.absPath)) {
const projectStore = new ProjectStore(
MessageAdapterFactory.createAdapter(lyraProjectConfig),
new YamlTranslationAdapter(lyraProjectConfig.absTranslationsPath),
);
globalThis.store.addProjectStore(lyraProjectConfig.absPath, projectStore);
Expand Down
11 changes: 10 additions & 1 deletion webapp/src/RepoGit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ export class RepoGit {

private readonly git: IGit;
private lyraConfig?: LyraConfig;
private lastPullTime: Date;

private constructor(private readonly spConfig: ServerProjectConfig) {
this.git = new SimpleGitWrapper(spConfig.repoPath);
this.lastPullTime = new Date(0);
}

static async getRepoGit(spConfig: ServerProjectConfig): Promise<RepoGit> {
Expand Down Expand Up @@ -67,7 +69,14 @@ export class RepoGit {
*/
public async checkoutBaseAndPull(): Promise<string> {
await this.git.checkout(this.spConfig.baseBranch);
await this.git.pull();

const now = new Date();
const age = now.getTime() - this.lastPullTime.getTime();
if (age > 30000) {
// We only pull if old
await this.git.pull();
this.lastPullTime = now;
}
return this.spConfig.baseBranch;
}

Expand Down
19 changes: 9 additions & 10 deletions webapp/src/actions/updateTranslation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { Cache } from '@/Cache';
import { RepoGit } from '@/RepoGit';
import { ServerConfig } from '@/utils/serverConfig';
import MessageAdapterFactory from '@/utils/adapters/MessageAdapterFactory';
import { MessageNotFound } from '@/errors';

export type TranslationSuccess = {
Expand Down Expand Up @@ -69,15 +68,6 @@ export default async function updateTranslation(
const repoGit = await RepoGit.getRepoGit(project);
const lyraConfig = await repoGit.getLyraConfig();
const projectConfig = lyraConfig.getProjectConfigByPath(project.projectPath);
const msgAdapter = MessageAdapterFactory.createAdapter(projectConfig);
const messages = await msgAdapter.getMessages();

const messageIds = messages.map((message) => message.id);
const foundId = messageIds.find((id) => id == messageId);

if (foundId === undefined) {
throw new MessageNotFound(languageName, messageId);
}

if (!projectConfig.isLanguageSupported(languageName)) {
return {
Expand All @@ -89,6 +79,15 @@ export default async function updateTranslation(
}

const projectStore = await Cache.getProjectStore(projectConfig);

const messages = await projectStore.getMessages();
const messageIds = messages.map((message) => message.id);
const foundId = messageIds.find((id) => id == messageId);

if (foundId === undefined) {
throw new MessageNotFound(languageName, messageId);
}

try {
await projectStore.updateTranslation(languageName, messageId, translation);
} catch (e) {
Expand Down
10 changes: 5 additions & 5 deletions webapp/src/dataAccess.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Cache } from '@/Cache';
import MessageAdapterFactory from '@/utils/adapters/MessageAdapterFactory';
import { RepoGit } from '@/RepoGit';
import { ServerConfig, ServerProjectConfig } from '@/utils/serverConfig';
import { getTranslationsIdText } from './utils/translationObjectUtil';
Expand Down Expand Up @@ -39,10 +38,11 @@ export async function accessLanguage(

await RepoGit.cloneIfNotExist(project);
const repoGit = await RepoGit.getRepoGit(project);
await repoGit.checkoutBaseAndPull();
const lyraConfig = await repoGit.getLyraConfig();
const projectConfig = lyraConfig.getProjectConfigByPath(project.projectPath);
const msgAdapter = MessageAdapterFactory.createAdapter(projectConfig);
const messages = await msgAdapter.getMessages();
const projectStore = await Cache.getProjectStore(projectConfig);
const messages = await projectStore.getMessages();
const translationsWithFilePath = await Cache.getLanguage(
projectName,
languageName,
Expand All @@ -58,11 +58,11 @@ export async function accessLanguage(
async function readProject(project: ServerProjectConfig) {
await RepoGit.cloneIfNotExist(project);
const repoGit = await RepoGit.getRepoGit(project);
await repoGit.checkoutBaseAndPull();
const lyraConfig = await repoGit.getLyraConfig();
const projectConfig = lyraConfig.getProjectConfigByPath(project.projectPath);
const msgAdapter = MessageAdapterFactory.createAdapter(projectConfig);
const messages = await msgAdapter.getMessages();
const store = await Cache.getProjectStore(projectConfig);
const messages = await store.getMessages();
const languagesWithTranslations = projectConfig.languages.map(
async (lang) => {
const translations = await store.getTranslations(lang);
Expand Down
71 changes: 63 additions & 8 deletions webapp/src/store/ProjectStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { describe, expect, it } from '@jest/globals';
import { describe, expect, it, jest } from '@jest/globals';

import { ProjectStore } from './ProjectStore';
import { LanguageNotFound } from '@/errors';
import { IMessageAdapter } from '@/utils/adapters';

function mockMsgAdapter(): jest.Mocked<IMessageAdapter> {
return {
getMessages: jest.fn<IMessageAdapter['getMessages']>(),
};
}

describe('ProjectStore', () => {
it('returns empty object when empty', async () => {
const projectStore = new ProjectStore({
const msgAdapter = mockMsgAdapter();
msgAdapter.getMessages.mockResolvedValue([]);

const projectStore = new ProjectStore(msgAdapter, {
getTranslations: async () => ({
sv: {},
}),
Expand All @@ -16,7 +26,16 @@ describe('ProjectStore', () => {
});

it('returns correct language', async () => {
const projectStore = new ProjectStore({
const msgAdapter = mockMsgAdapter();
msgAdapter.getMessages.mockResolvedValue([
{
defaultMessage: '',
id: 'greeting.headline',
params: [],
},
]);

const projectStore = new ProjectStore(msgAdapter, {
getTranslations: async () => ({
de: {
'greeting.headline': {
Expand All @@ -41,7 +60,9 @@ describe('ProjectStore', () => {

it('throws exception for missing language', async () => {
expect.assertions(1);
const projectStore = new ProjectStore({
const msgAdapter = mockMsgAdapter();
msgAdapter.getMessages.mockResolvedValue([]);
const projectStore = new ProjectStore(msgAdapter, {
getTranslations: async () => ({}),
});

Expand All @@ -50,7 +71,16 @@ describe('ProjectStore', () => {
});

it('returns updated translations', async () => {
const projectStore = new ProjectStore({
const msgAdapter = mockMsgAdapter();
msgAdapter.getMessages.mockResolvedValue([
{
defaultMessage: '',
id: 'greeting.headline',
params: [],
},
]);

const projectStore = new ProjectStore(msgAdapter, {
getTranslations: async () => ({
de: {
'greeting.headline': {
Expand Down Expand Up @@ -80,7 +110,16 @@ describe('ProjectStore', () => {
});

it('can update translations before getTranslations()', async () => {
const projectStore = new ProjectStore({
const msgAdapter = mockMsgAdapter();
msgAdapter.getMessages.mockResolvedValue([
{
defaultMessage: '',
id: 'greeting.headline',
params: [],
},
]);

const projectStore = new ProjectStore(msgAdapter, {
getTranslations: async () => ({
de: {
'greeting.headline': {
Expand All @@ -104,7 +143,15 @@ describe('ProjectStore', () => {

it('throws exception for missing language', async () => {
expect.assertions(1);
const projectStore = new ProjectStore({
const msgAdapter = mockMsgAdapter();
msgAdapter.getMessages.mockResolvedValue([
{
defaultMessage: '',
id: 'greeting.headline',
params: [],
},
]);
const projectStore = new ProjectStore(msgAdapter, {
getTranslations: async () => ({}),
});

Expand All @@ -118,7 +165,15 @@ describe('ProjectStore', () => {
});

it('gives full access to all languages', async () => {
const projectStore = new ProjectStore({
const msgAdapter = mockMsgAdapter();
msgAdapter.getMessages.mockResolvedValue([
{
defaultMessage: '',
id: 'greeting.headline',
params: [],
},
]);
const projectStore = new ProjectStore(msgAdapter, {
getTranslations: async () => ({
de: {
'greeting.headline': {
Expand Down
40 changes: 27 additions & 13 deletions webapp/src/store/ProjectStore.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import {
IMessageAdapter,
ITranslationAdapter,
MessageData,
MessageMap,
TranslationMap,
} from '@/utils/adapters';
import { LanguageNotFound } from '@/errors';

type StoreData = {
languages: TranslationMap;
};
import { StoreData } from './types';
import mergeStoreData from './mergeStoreData';

export class ProjectStore {
private readonly data: StoreData;
private data: StoreData;
private readonly translationAdapter: ITranslationAdapter;
private readonly messageAdapter: IMessageAdapter;

constructor(translationAdapter: ITranslationAdapter) {
constructor(
messageAdapter: IMessageAdapter,
translationAdapter: ITranslationAdapter,
) {
this.data = {
languages: {},
messages: [],
};

this.translationAdapter = translationAdapter;
this.messageAdapter = messageAdapter;
}

async getLanguageData(): Promise<TranslationMap> {
await this.initIfNecessary();
await this.refresh();

const output: TranslationMap = {};
for await (const lang of Object.keys(this.data.languages)) {
Expand All @@ -33,7 +39,7 @@ export class ProjectStore {
}

async getTranslations(lang: string): Promise<MessageMap> {
await this.initIfNecessary();
await this.refresh();

const language = this.data.languages[lang];
if (!language) {
Expand All @@ -48,8 +54,13 @@ export class ProjectStore {
return output;
}

async getMessages(): Promise<MessageData[]> {
await this.refresh();
return this.data.messages;
}

async updateTranslation(lang: string, id: string, text: string) {
await this.initIfNecessary();
await this.refresh();

if (!this.data.languages[lang]) {
throw new LanguageNotFound(lang);
Expand All @@ -63,10 +74,13 @@ export class ProjectStore {
this.data.languages[lang][id].text = text;
}

private async initIfNecessary() {
if (Object.keys(this.data.languages).length == 0) {
this.data.languages = await this.translationAdapter.getTranslations();
}
private async refresh() {
const fromRepo: StoreData = {
languages: await this.translationAdapter.getTranslations(),
messages: await this.messageAdapter.getMessages(),
};

this.data = mergeStoreData(this.data, fromRepo);
}

/** get the source file from the default en language otherwise generate one from locale root*/
Expand Down
13 changes: 10 additions & 3 deletions webapp/src/store/Store.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { describe, expect, it } from '@jest/globals';
import { describe, expect, it, jest } from '@jest/globals';

import { ProjectStore } from '@/store/ProjectStore';
import { Store } from './Store';
import { IMessageAdapter } from '@/utils/adapters';

describe('Store', () => {
const mockMsgAdapter: jest.Mocked<IMessageAdapter> = {
getMessages: jest
.fn<IMessageAdapter['getMessages']>()
.mockResolvedValue([]),
};

describe('getProjectStore()', () => {
it('get ProjectStore for a path', async () => {
const store = new Store();
const projectStore = new ProjectStore({
const projectStore = new ProjectStore(mockMsgAdapter, {
getTranslations: async () => ({
de: {
'greeting.headline': {
Expand All @@ -31,7 +38,7 @@ describe('Store', () => {
describe('hasProjectStore()', () => {
it('get ProjectStore for a path', async () => {
const store = new Store();
const projectStore = new ProjectStore({
const projectStore = new ProjectStore(mockMsgAdapter, {
getTranslations: async () => ({
de: {
'greeting.headline': {
Expand Down
Loading

0 comments on commit 4e3bccb

Please sign in to comment.