@actual-app/core 26.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swcrc +11 -0
- package/bin/build-browser +40 -0
- package/bin/copy-migrations +9 -0
- package/db.sqlite +0 -0
- package/default-db.sqlite +0 -0
- package/migrations/.force-copy-windows +0 -0
- package/migrations/1548957970627_remove-db-version.sql +5 -0
- package/migrations/1550601598648_payees.sql +23 -0
- package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
- package/migrations/1561751833510_indexes.sql +7 -0
- package/migrations/1567699552727_budget.sql +38 -0
- package/migrations/1582384163573_cleared.sql +6 -0
- package/migrations/1597756566448_rules.sql +10 -0
- package/migrations/1608652596043_parent_field.sql +13 -0
- package/migrations/1608652596044_trans_views.sql +56 -0
- package/migrations/1612625548236_optimize.sql +7 -0
- package/migrations/1614782639336_trans_views2.sql +33 -0
- package/migrations/1615745967948_meta.sql +10 -0
- package/migrations/1616167010796_accounts_order.sql +5 -0
- package/migrations/1618975177358_schedules.sql +28 -0
- package/migrations/1632571489012_remove_cache.js +136 -0
- package/migrations/1679728867040_rules_conditions.sql +5 -0
- package/migrations/1681115033845_add_schedule_name.sql +5 -0
- package/migrations/1682974838138_remove_payee_rules.sql +5 -0
- package/migrations/1685007876842_add_category_hidden.sql +6 -0
- package/migrations/1686139660866_remove_account_type.sql +5 -0
- package/migrations/1688749527273_transaction_filters.sql +10 -0
- package/migrations/1688841238000_add_account_type.sql +5 -0
- package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
- package/migrations/1694438752000_add_goal_targets.sql +7 -0
- package/migrations/1697046240000_add_reconciled.sql +5 -0
- package/migrations/1704572023730_add_account_sync_source.sql +5 -0
- package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
- package/migrations/1707267033000_reports.sql +28 -0
- package/migrations/1712784523000_unhide_input_group.sql +8 -0
- package/migrations/1716359441000_include_current.sql +5 -0
- package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
- package/migrations/1720664867241_add_payee_favorite.sql +5 -0
- package/migrations/1720665000000_goal_context.sql +6 -0
- package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
- package/migrations/1722804019000_create_dashboard_table.js +69 -0
- package/migrations/1723665565000_prefs.js +59 -0
- package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
- package/migrations/1736640000000_custom_report_sorting.sql +7 -0
- package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
- package/migrations/1738491452000_sorting_rename.sql +13 -0
- package/migrations/1739139550000_bank_sync_page.sql +7 -0
- package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
- package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
- package/migrations/1749799110000_add_tags.sql +10 -0
- package/migrations/1749799110001_tags_tombstone.sql +5 -0
- package/migrations/1754611200000_add_category_template_settings.sql +5 -0
- package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
- package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
- package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
- package/migrations/1765518577215_multiple_dashboards.js +30 -0
- package/migrations/1768872504000_add_payee_locations.sql +21 -0
- package/package.json +128 -0
- package/src/mocks/arbitrary-schema.ts +162 -0
- package/src/mocks/budget.ts +901 -0
- package/src/mocks/files/8859-1.qfx +63 -0
- package/src/mocks/files/best.data-ever$.QFX +124 -0
- package/src/mocks/files/big.data.QiF +91 -0
- package/src/mocks/files/budgets/.commit-to-git +0 -0
- package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
- package/src/mocks/files/camt/camt.053.xml +463 -0
- package/src/mocks/files/credit-card.ofx +11 -0
- package/src/mocks/files/data-multi-decimal.ofx +64 -0
- package/src/mocks/files/data-payee-memo.ofx +75 -0
- package/src/mocks/files/data-payee-memo.qif +17 -0
- package/src/mocks/files/data.ofx +124 -0
- package/src/mocks/files/data.qfx +124 -0
- package/src/mocks/files/data.qif +91 -0
- package/src/mocks/files/default-budget-template/db.sqlite +0 -0
- package/src/mocks/files/default-budget-template/metadata.json +6 -0
- package/src/mocks/files/html-vals.qfx +17 -0
- package/src/mocks/index.ts +221 -0
- package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
- package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
- package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
- package/src/mocks/random.ts +16 -0
- package/src/mocks/setup.ts +180 -0
- package/src/mocks/spreadsheet.ts +101 -0
- package/src/mocks/util.ts +82 -0
- package/src/platform/client/connection/README.md +3 -0
- package/src/platform/client/connection/__mocks__/index.ts +67 -0
- package/src/platform/client/connection/index-types.ts +95 -0
- package/src/platform/client/connection/index.browser.ts +213 -0
- package/src/platform/client/connection/index.ts +155 -0
- package/src/platform/client/undo/index.ts +59 -0
- package/src/platform/exceptions/__mocks__/index.ts +7 -0
- package/src/platform/exceptions/index.ts +9 -0
- package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
- package/src/platform/server/asyncStorage/index-types.ts +35 -0
- package/src/platform/server/asyncStorage/index.api.ts +2 -0
- package/src/platform/server/asyncStorage/index.electron.ts +88 -0
- package/src/platform/server/asyncStorage/index.ts +126 -0
- package/src/platform/server/connection/README.md +3 -0
- package/src/platform/server/connection/__mocks__/index.ts +15 -0
- package/src/platform/server/connection/index-types.ts +20 -0
- package/src/platform/server/connection/index.api.ts +13 -0
- package/src/platform/server/connection/index.electron.ts +102 -0
- package/src/platform/server/connection/index.ts +154 -0
- package/src/platform/server/fetch/__mocks__/index.ts +3 -0
- package/src/platform/server/fetch/index.api.ts +1 -0
- package/src/platform/server/fetch/index.electron.ts +18 -0
- package/src/platform/server/fetch/index.ts +20 -0
- package/src/platform/server/fs/index.api.ts +198 -0
- package/src/platform/server/fs/index.electron.ts +208 -0
- package/src/platform/server/fs/index.test.ts +117 -0
- package/src/platform/server/fs/index.ts +416 -0
- package/src/platform/server/fs/path-join.api.ts +1 -0
- package/src/platform/server/fs/path-join.electron.ts +1 -0
- package/src/platform/server/fs/path-join.ts +97 -0
- package/src/platform/server/fs/shared.ts +33 -0
- package/src/platform/server/indexeddb/index.ts +115 -0
- package/src/platform/server/log/index.ts +43 -0
- package/src/platform/server/sqlite/index.api.ts +2 -0
- package/src/platform/server/sqlite/index.electron.ts +134 -0
- package/src/platform/server/sqlite/index.test.ts +108 -0
- package/src/platform/server/sqlite/index.ts +241 -0
- package/src/platform/server/sqlite/normalise.ts +9 -0
- package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
- package/src/platform/server/sqlite/unicodeLike.ts +31 -0
- package/src/server/__mocks__/post.ts +9 -0
- package/src/server/__snapshots__/main.test.ts.snap +199 -0
- package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
- package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
- package/src/server/accounts/app-bank-sync.test.ts +136 -0
- package/src/server/accounts/app.ts +1294 -0
- package/src/server/accounts/link.ts +25 -0
- package/src/server/accounts/payees.ts +36 -0
- package/src/server/accounts/sync.test.ts +679 -0
- package/src/server/accounts/sync.ts +1168 -0
- package/src/server/accounts/title/index.ts +60 -0
- package/src/server/accounts/title/lower-case.ts +93 -0
- package/src/server/accounts/title/specials.ts +21 -0
- package/src/server/admin/app.ts +241 -0
- package/src/server/api-models.ts +244 -0
- package/src/server/api.test.ts +36 -0
- package/src/server/api.ts +1030 -0
- package/src/server/app.ts +91 -0
- package/src/server/aql/compiler.test.ts +966 -0
- package/src/server/aql/compiler.ts +1222 -0
- package/src/server/aql/exec.test.ts +289 -0
- package/src/server/aql/exec.ts +128 -0
- package/src/server/aql/index.ts +41 -0
- package/src/server/aql/schema/executors.test.ts +420 -0
- package/src/server/aql/schema/executors.ts +345 -0
- package/src/server/aql/schema/index.test.ts +67 -0
- package/src/server/aql/schema/index.ts +409 -0
- package/src/server/aql/schema-helpers.test.ts +242 -0
- package/src/server/aql/schema-helpers.ts +208 -0
- package/src/server/aql/views.test.ts +62 -0
- package/src/server/aql/views.ts +57 -0
- package/src/server/auth/app.ts +387 -0
- package/src/server/bench.ts +29 -0
- package/src/server/budget/actions.ts +686 -0
- package/src/server/budget/app.ts +469 -0
- package/src/server/budget/base.test.ts +340 -0
- package/src/server/budget/base.ts +339 -0
- package/src/server/budget/category-template-context.test.ts +1658 -0
- package/src/server/budget/category-template-context.ts +862 -0
- package/src/server/budget/cleanup-template.pegjs +27 -0
- package/src/server/budget/cleanup-template.ts +408 -0
- package/src/server/budget/envelope.ts +403 -0
- package/src/server/budget/goal-template.pegjs +110 -0
- package/src/server/budget/goal-template.ts +309 -0
- package/src/server/budget/report.ts +308 -0
- package/src/server/budget/schedule-template.test.ts +184 -0
- package/src/server/budget/schedule-template.ts +351 -0
- package/src/server/budget/statements.ts +60 -0
- package/src/server/budget/template-notes.test.ts +393 -0
- package/src/server/budget/template-notes.ts +323 -0
- package/src/server/budget/util.ts +25 -0
- package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
- package/src/server/budgetfiles/app.ts +672 -0
- package/src/server/budgetfiles/backups.test.ts +79 -0
- package/src/server/budgetfiles/backups.ts +251 -0
- package/src/server/cloud-storage.ts +467 -0
- package/src/server/dashboard/app.ts +373 -0
- package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
- package/src/server/db/index.test.ts +300 -0
- package/src/server/db/index.ts +855 -0
- package/src/server/db/mappings.ts +59 -0
- package/src/server/db/sort.ts +58 -0
- package/src/server/db/types/index.ts +342 -0
- package/src/server/db/util.ts +36 -0
- package/src/server/encryption/app.ts +133 -0
- package/src/server/encryption/encryption-internals.api.ts +2 -0
- package/src/server/encryption/encryption-internals.electron.ts +89 -0
- package/src/server/encryption/encryption-internals.ts +109 -0
- package/src/server/encryption/encryption.test.ts +19 -0
- package/src/server/encryption/index.test.ts +19 -0
- package/src/server/encryption/index.ts +89 -0
- package/src/server/errors.ts +110 -0
- package/src/server/filters/app.ts +191 -0
- package/src/server/importers/actual.ts +49 -0
- package/src/server/importers/index.ts +58 -0
- package/src/server/importers/ynab4-types.ts +163 -0
- package/src/server/importers/ynab4.ts +470 -0
- package/src/server/importers/ynab5-types.ts +290 -0
- package/src/server/importers/ynab5.ts +1193 -0
- package/src/server/main-app.ts +25 -0
- package/src/server/main.test.ts +392 -0
- package/src/server/main.ts +336 -0
- package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
- package/src/server/migrate/cli.ts +100 -0
- package/src/server/migrate/migrations.test.ts +81 -0
- package/src/server/migrate/migrations.ts +192 -0
- package/src/server/models.ts +184 -0
- package/src/server/mutators.ts +139 -0
- package/src/server/notes/app.ts +18 -0
- package/src/server/payees/app.ts +351 -0
- package/src/server/polyfills.ts +26 -0
- package/src/server/post.ts +219 -0
- package/src/server/preferences/app.ts +249 -0
- package/src/server/prefs.ts +91 -0
- package/src/server/reports/app.ts +187 -0
- package/src/server/rules/action.ts +344 -0
- package/src/server/rules/app.ts +193 -0
- package/src/server/rules/condition.ts +436 -0
- package/src/server/rules/customFunctions.ts +61 -0
- package/src/server/rules/formula-action.test.ts +175 -0
- package/src/server/rules/handlebars-helpers.ts +131 -0
- package/src/server/rules/index.test.ts +1095 -0
- package/src/server/rules/index.ts +22 -0
- package/src/server/rules/rule-indexer.ts +89 -0
- package/src/server/rules/rule-utils.ts +274 -0
- package/src/server/rules/rule.ts +193 -0
- package/src/server/schedules/app.test.ts +502 -0
- package/src/server/schedules/app.ts +644 -0
- package/src/server/schedules/find-schedules.ts +391 -0
- package/src/server/server-config.ts +59 -0
- package/src/server/sheet.test.ts +101 -0
- package/src/server/sheet.ts +280 -0
- package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
- package/src/server/spreadsheet/app.ts +54 -0
- package/src/server/spreadsheet/globals.ts +13 -0
- package/src/server/spreadsheet/graph-data-structure.ts +165 -0
- package/src/server/spreadsheet/scratch +60 -0
- package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
- package/src/server/spreadsheet/spreadsheet.ts +523 -0
- package/src/server/spreadsheet/util.ts +15 -0
- package/src/server/sql/init.sql +88 -0
- package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
- package/src/server/sync/app.ts +29 -0
- package/src/server/sync/encoder.ts +129 -0
- package/src/server/sync/index.ts +820 -0
- package/src/server/sync/make-test-message.ts +19 -0
- package/src/server/sync/migrate.test.ts +169 -0
- package/src/server/sync/migrate.ts +48 -0
- package/src/server/sync/repair.ts +39 -0
- package/src/server/sync/reset.ts +91 -0
- package/src/server/sync/sync.property.test.ts +385 -0
- package/src/server/sync/sync.test.ts +349 -0
- package/src/server/sync/utils.ts +3 -0
- package/src/server/tags/app.ts +101 -0
- package/src/server/tests/mockData.json +9352 -0
- package/src/server/tests/mockSyncServer.ts +119 -0
- package/src/server/tools/app.ts +152 -0
- package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
- package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
- package/src/server/transactions/app.ts +136 -0
- package/src/server/transactions/export/export-to-csv.ts +132 -0
- package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
- package/src/server/transactions/import/ofx2json.test.ts +33 -0
- package/src/server/transactions/import/ofx2json.ts +157 -0
- package/src/server/transactions/import/parse-file.test.ts +224 -0
- package/src/server/transactions/import/parse-file.ts +286 -0
- package/src/server/transactions/import/qif2json.ts +110 -0
- package/src/server/transactions/import/xmlcamt2json.ts +168 -0
- package/src/server/transactions/index.ts +196 -0
- package/src/server/transactions/merge.test.ts +370 -0
- package/src/server/transactions/merge.ts +139 -0
- package/src/server/transactions/transaction-rules.test.ts +994 -0
- package/src/server/transactions/transaction-rules.ts +1038 -0
- package/src/server/transactions/transfer.test.ts +221 -0
- package/src/server/transactions/transfer.ts +173 -0
- package/src/server/undo.ts +271 -0
- package/src/server/update.ts +37 -0
- package/src/server/util/budget-name.ts +61 -0
- package/src/server/util/custom-sync-mapping.ts +48 -0
- package/src/server/util/rschedule.ts +9 -0
- package/src/shared/__mocks__/platform.ts +7 -0
- package/src/shared/__snapshots__/months.test.ts.snap +21 -0
- package/src/shared/arithmetic.test.ts +112 -0
- package/src/shared/arithmetic.ts +170 -0
- package/src/shared/async.test.ts +135 -0
- package/src/shared/async.ts +76 -0
- package/src/shared/constants.ts +5 -0
- package/src/shared/currencies.ts +70 -0
- package/src/shared/dashboard.ts +260 -0
- package/src/shared/environment.ts +18 -0
- package/src/shared/errors.ts +195 -0
- package/src/shared/locale.ts +27 -0
- package/src/shared/location-utils.test.ts +69 -0
- package/src/shared/location-utils.ts +49 -0
- package/src/shared/months.test.ts +5 -0
- package/src/shared/months.ts +485 -0
- package/src/shared/normalisation.ts +6 -0
- package/src/shared/platform.electron.ts +21 -0
- package/src/shared/platform.ts +20 -0
- package/src/shared/query.ts +176 -0
- package/src/shared/rules.test.ts +56 -0
- package/src/shared/rules.ts +371 -0
- package/src/shared/schedules.test.ts +570 -0
- package/src/shared/schedules.ts +560 -0
- package/src/shared/test-helpers.ts +156 -0
- package/src/shared/transactions.test.ts +275 -0
- package/src/shared/transactions.ts +433 -0
- package/src/shared/transfer.test.ts +75 -0
- package/src/shared/transfer.ts +16 -0
- package/src/shared/user.ts +4 -0
- package/src/shared/util.test.ts +240 -0
- package/src/shared/util.ts +633 -0
- package/src/types/api-handlers.ts +287 -0
- package/src/types/budget.ts +8 -0
- package/src/types/file.ts +47 -0
- package/src/types/handlers.ts +46 -0
- package/src/types/models/account.ts +24 -0
- package/src/types/models/bank-sync.ts +23 -0
- package/src/types/models/bank.ts +6 -0
- package/src/types/models/category-group.ts +11 -0
- package/src/types/models/category.ts +13 -0
- package/src/types/models/dashboard.ts +199 -0
- package/src/types/models/gocardless.ts +84 -0
- package/src/types/models/import-transaction.ts +56 -0
- package/src/types/models/index.ts +23 -0
- package/src/types/models/nearby-payee.ts +7 -0
- package/src/types/models/note.ts +4 -0
- package/src/types/models/openid.ts +8 -0
- package/src/types/models/payee-location.ts +8 -0
- package/src/types/models/payee.ts +10 -0
- package/src/types/models/pluggyai.ts +19 -0
- package/src/types/models/reports.ts +144 -0
- package/src/types/models/rule.ts +174 -0
- package/src/types/models/schedule.ts +49 -0
- package/src/types/models/simplefin.ts +28 -0
- package/src/types/models/tags.ts +6 -0
- package/src/types/models/templates.ts +135 -0
- package/src/types/models/transaction-filter.ts +9 -0
- package/src/types/models/transaction.ts +39 -0
- package/src/types/models/user-access.ts +10 -0
- package/src/types/models/user.ts +25 -0
- package/src/types/prefs.ts +167 -0
- package/src/types/server-events.ts +86 -0
- package/src/types/server-handlers.ts +27 -0
- package/src/types/util.ts +26 -0
- package/tsconfig.json +34 -0
- package/typings/pegjs.ts +1 -0
- package/typings/process-worker.ts +12 -0
- package/typings/vite-plugin-peggy-loader.ts +1 -0
- package/typings/window.ts +62 -0
- package/vite.config.ts +109 -0
- package/vite.desktop.config.ts +59 -0
- package/vitest.config.ts +43 -0
- package/vitest.web.config.ts +38 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
|
|
3
|
+
import * as fs from '../../platform/server/fs';
|
|
4
|
+
import { handlers } from '../main';
|
|
5
|
+
|
|
6
|
+
export async function uniqueBudgetName(
|
|
7
|
+
initialName: string = 'My Finances',
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
const budgets = await handlers['get-budgets']();
|
|
10
|
+
let idx = 1;
|
|
11
|
+
|
|
12
|
+
// If there is a conflict, keep appending an index until there is no
|
|
13
|
+
// conflict and we have a unique name
|
|
14
|
+
let newName = initialName;
|
|
15
|
+
while (budgets.find(file => file.name === newName)) {
|
|
16
|
+
newName = `${initialName} ${idx}`;
|
|
17
|
+
idx++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return newName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function validateBudgetName(
|
|
24
|
+
name: string,
|
|
25
|
+
): Promise<{ valid: boolean; message?: string }> {
|
|
26
|
+
const trimmedName = name.trim();
|
|
27
|
+
const uniqueName = await uniqueBudgetName(trimmedName);
|
|
28
|
+
let message: string | null = null;
|
|
29
|
+
|
|
30
|
+
if (trimmedName === '') message = 'Budget name cannot be blank';
|
|
31
|
+
if (trimmedName.length > 100) {
|
|
32
|
+
message = 'Budget name is too long (max length 100)';
|
|
33
|
+
}
|
|
34
|
+
if (uniqueName !== trimmedName) {
|
|
35
|
+
message = `"${name}" already exists, try "${uniqueName}" instead`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return message ? { valid: false, message } : { valid: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function idFromBudgetName(name: string): Promise<string> {
|
|
42
|
+
let id = name.replace(/( |[^A-Za-z0-9])/g, '-') + '-' + uuidv4().slice(0, 7);
|
|
43
|
+
|
|
44
|
+
// Make sure the id is unique. There's a chance one could already
|
|
45
|
+
// exist (although very unlikely now that we append unique
|
|
46
|
+
// characters onto the id)
|
|
47
|
+
let index = 0;
|
|
48
|
+
|
|
49
|
+
let budgetDir = fs.getBudgetDir(id);
|
|
50
|
+
while (await fs.exists(budgetDir)) {
|
|
51
|
+
index++;
|
|
52
|
+
budgetDir = fs.getBudgetDir(id + index.toString());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If a suffix was added, update the id
|
|
56
|
+
if (index > 0) {
|
|
57
|
+
id = id + index.toString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type Mappings = Map<string, Map<string, string>>;
|
|
2
|
+
|
|
3
|
+
export const mappingsToString = (mapping: Mappings): string =>
|
|
4
|
+
JSON.stringify(
|
|
5
|
+
Object.fromEntries(
|
|
6
|
+
[...mapping.entries()].map(([key, value]) => [
|
|
7
|
+
key,
|
|
8
|
+
Object.fromEntries(value),
|
|
9
|
+
]),
|
|
10
|
+
),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export const mappingsFromString = (str: string): Mappings => {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(str);
|
|
16
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
17
|
+
throw new Error('Invalid mapping format');
|
|
18
|
+
}
|
|
19
|
+
return new Map(
|
|
20
|
+
Object.entries(parsed).map(([key, value]) => [
|
|
21
|
+
key,
|
|
22
|
+
new Map(Object.entries(value as object)),
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
const message = e instanceof Error ? e.message : e;
|
|
27
|
+
throw new Error(`Failed to parse mapping: ${String(message)}`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const defaultMappings: Mappings = new Map([
|
|
32
|
+
[
|
|
33
|
+
'payment',
|
|
34
|
+
new Map([
|
|
35
|
+
['date', 'date'],
|
|
36
|
+
['payee', 'payeeName'],
|
|
37
|
+
['notes', 'notes'],
|
|
38
|
+
]),
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
'deposit',
|
|
42
|
+
new Map([
|
|
43
|
+
['date', 'date'],
|
|
44
|
+
['payee', 'payeeName'],
|
|
45
|
+
['notes', 'notes'],
|
|
46
|
+
]),
|
|
47
|
+
],
|
|
48
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import '@rschedule/standard-date-adapter/setup';
|
|
2
|
+
import { Schedule as OriginalSchedule } from '@rschedule/core/generators';
|
|
3
|
+
|
|
4
|
+
export * from '@rschedule/standard-date-adapter';
|
|
5
|
+
export * from '@rschedule/core';
|
|
6
|
+
export * from '@rschedule/core/generators';
|
|
7
|
+
|
|
8
|
+
// Creates a wrapper class to ensure constructor behavior when bundled with vite
|
|
9
|
+
export class RSchedule extends OriginalSchedule {}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`range returns a full range 1`] = `
|
|
4
|
+
[
|
|
5
|
+
"2016-10",
|
|
6
|
+
"2016-11",
|
|
7
|
+
"2016-12",
|
|
8
|
+
"2017-01",
|
|
9
|
+
"2017-02",
|
|
10
|
+
"2017-03",
|
|
11
|
+
"2017-04",
|
|
12
|
+
"2017-05",
|
|
13
|
+
"2017-06",
|
|
14
|
+
"2017-07",
|
|
15
|
+
"2017-08",
|
|
16
|
+
"2017-09",
|
|
17
|
+
"2017-10",
|
|
18
|
+
"2017-11",
|
|
19
|
+
"2017-12",
|
|
20
|
+
]
|
|
21
|
+
`;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { evalArithmetic } from './arithmetic';
|
|
2
|
+
import { setNumberFormat } from './util';
|
|
3
|
+
|
|
4
|
+
describe('arithmetic', () => {
|
|
5
|
+
test('handles negative numbers', () => {
|
|
6
|
+
expect(evalArithmetic('-4')).toBe(-4);
|
|
7
|
+
expect(evalArithmetic('10 + -4')).toBe(6);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('handles simple addition', () => {
|
|
11
|
+
expect(evalArithmetic('10 + 10')).toEqual(20);
|
|
12
|
+
expect(evalArithmetic('1.5 + 1.5')).toEqual(3);
|
|
13
|
+
expect(evalArithmetic('(12 + 3) + (10)')).toEqual(25);
|
|
14
|
+
expect(evalArithmetic('10 + 20 + 30 + 40')).toEqual(100);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('handles simple subtraction', () => {
|
|
18
|
+
expect(evalArithmetic('10 - 10')).toEqual(0);
|
|
19
|
+
expect(evalArithmetic('4.5 - 1.5')).toEqual(3);
|
|
20
|
+
expect(evalArithmetic('(12 - 3) - (10)')).toEqual(-1);
|
|
21
|
+
expect(evalArithmetic('10 - 20 - 30 - 40')).toEqual(-80);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('handles multiplication', () => {
|
|
25
|
+
expect(evalArithmetic('10 * 10')).toEqual(100);
|
|
26
|
+
expect(evalArithmetic('1.5 * 1.5')).toEqual(2.25);
|
|
27
|
+
expect(evalArithmetic('10 * 20 * 30 * 40')).toEqual(240000);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('handles division', () => {
|
|
31
|
+
expect(evalArithmetic('10 / 10')).toEqual(1);
|
|
32
|
+
expect(evalArithmetic('1.5 / .5')).toEqual(3);
|
|
33
|
+
expect(evalArithmetic('2400 / 2 / 5')).toEqual(240);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('handles order of operations', () => {
|
|
37
|
+
expect(evalArithmetic('(5 + 3) * 10')).toEqual(80);
|
|
38
|
+
expect(evalArithmetic('5 + 3 * 10')).toEqual(35);
|
|
39
|
+
expect(evalArithmetic('20^3 - 5 * (10 / 2)')).toEqual(7975);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('handles exponent as right-associative', () => {
|
|
43
|
+
expect(evalArithmetic('2^3^2')).toEqual(512);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('handles same-precedence operators left-to-right', () => {
|
|
47
|
+
expect(evalArithmetic('24 / 3 * 2')).toEqual(16);
|
|
48
|
+
expect(evalArithmetic('24 * 3 / 2')).toEqual(36);
|
|
49
|
+
expect(evalArithmetic('10 - 2 + 1')).toEqual(9);
|
|
50
|
+
expect(evalArithmetic('10 + 2 - 1')).toEqual(11);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('respects current number format', () => {
|
|
54
|
+
expect(evalArithmetic('1,222.45')).toEqual(1222.45);
|
|
55
|
+
|
|
56
|
+
setNumberFormat({ format: 'space-comma', hideFraction: false });
|
|
57
|
+
expect(evalArithmetic('1\u202F222,45')).toEqual(1222.45);
|
|
58
|
+
|
|
59
|
+
setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
|
|
60
|
+
expect(evalArithmetic(`1\u2019222.45`)).toEqual(1222.45);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('ignores leftover characters', () => {
|
|
64
|
+
expect(evalArithmetic('1+2)')).toBe(3);
|
|
65
|
+
expect(evalArithmetic('1+2)foo')).toBe(3);
|
|
66
|
+
expect(evalArithmetic('(1+2)x')).toBe(3);
|
|
67
|
+
expect(evalArithmetic('10+20 trailing')).toBe(30);
|
|
68
|
+
expect(evalArithmetic('1+2(3')).toBe(3);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('handles apostrophe-dot format with keyboard apostrophe (U+0027)', () => {
|
|
72
|
+
setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
|
|
73
|
+
|
|
74
|
+
// Test with keyboard apostrophe (U+0027) - what users type
|
|
75
|
+
const keyboardApostrophe = '12\u0027345.67';
|
|
76
|
+
expect(keyboardApostrophe.charCodeAt(2)).toBe(0x0027); // Verify it's U+0027
|
|
77
|
+
expect(evalArithmetic(keyboardApostrophe)).toBe(12345.67);
|
|
78
|
+
|
|
79
|
+
// More test cases with keyboard apostrophe
|
|
80
|
+
expect(evalArithmetic('1\u0027234.56')).toBe(1234.56);
|
|
81
|
+
expect(evalArithmetic('1\u0027000.33')).toBe(1000.33);
|
|
82
|
+
expect(evalArithmetic('100\u0027000.99')).toBe(100000.99);
|
|
83
|
+
expect(evalArithmetic('1\u0027000\u0027000.50')).toBe(1000000.5);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('handles apostrophe-dot format with typographic apostrophe (U+2019)', () => {
|
|
87
|
+
setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
|
|
88
|
+
|
|
89
|
+
// Test with right single quotation mark (U+2019) - what Intl.NumberFormat outputs
|
|
90
|
+
const intlApostrophe = '12\u2019345.67';
|
|
91
|
+
expect(intlApostrophe.charCodeAt(2)).toBe(0x2019); // Verify it's U+2019
|
|
92
|
+
expect(evalArithmetic(intlApostrophe)).toBe(12345.67);
|
|
93
|
+
|
|
94
|
+
// More test cases with typographic apostrophe
|
|
95
|
+
expect(evalArithmetic('1\u2019234.56')).toBe(1234.56);
|
|
96
|
+
expect(evalArithmetic('1\u2019000.33')).toBe(1000.33);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('handles apostrophe-dot format in arithmetic expressions', () => {
|
|
100
|
+
setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
|
|
101
|
+
|
|
102
|
+
// Test arithmetic operations with keyboard apostrophe
|
|
103
|
+
expect(evalArithmetic('1\u0027000 + 2\u0027000')).toBe(3000);
|
|
104
|
+
expect(evalArithmetic('10\u0027000 - 2\u0027500')).toBe(7500);
|
|
105
|
+
expect(evalArithmetic('1\u0027000 * 2')).toBe(2000);
|
|
106
|
+
expect(evalArithmetic('4\u0027000 / 2')).toBe(2000);
|
|
107
|
+
|
|
108
|
+
// Test arithmetic operations with typographic apostrophe
|
|
109
|
+
expect(evalArithmetic('1\u2019000 + 2\u2019000')).toBe(3000);
|
|
110
|
+
expect(evalArithmetic('10\u2019000 - 2\u2019500')).toBe(7500);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { currencyToAmount } from './util';
|
|
2
|
+
|
|
3
|
+
type ParserState = {
|
|
4
|
+
str: string;
|
|
5
|
+
index: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type Operator = '+' | '-' | '*' | '/' | '^';
|
|
9
|
+
|
|
10
|
+
type OperatorNode = {
|
|
11
|
+
op: Operator;
|
|
12
|
+
left: AstNode;
|
|
13
|
+
right: AstNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type AstNode = number | OperatorNode;
|
|
17
|
+
|
|
18
|
+
function fail(state: ParserState, msg: string): never {
|
|
19
|
+
throw new Error(
|
|
20
|
+
msg + ': ' + JSON.stringify(state.str.slice(state.index, 10)),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function char(state: ParserState): string | undefined {
|
|
25
|
+
return state.str[state.index];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function next(state: ParserState): string | null {
|
|
29
|
+
if (state.index >= state.str.length) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ch = char(state);
|
|
34
|
+
state.index++;
|
|
35
|
+
return ch ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nextOperator(state: ParserState, op: string): boolean {
|
|
39
|
+
if (char(state) === op) {
|
|
40
|
+
next(state);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parsePrimary(state: ParserState): number {
|
|
48
|
+
// We only support numbers
|
|
49
|
+
const isNegative = char(state) === '-';
|
|
50
|
+
if (isNegative) {
|
|
51
|
+
next(state);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let numberStr = '';
|
|
55
|
+
let currentChar = char(state);
|
|
56
|
+
while (
|
|
57
|
+
currentChar &&
|
|
58
|
+
currentChar.match(/[0-9,.'\u2019\u00A0\u202F ]|\p{Sc}/u)
|
|
59
|
+
) {
|
|
60
|
+
const ch = next(state);
|
|
61
|
+
if (ch !== null) {
|
|
62
|
+
numberStr += ch;
|
|
63
|
+
}
|
|
64
|
+
currentChar = char(state);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (numberStr === '') {
|
|
68
|
+
fail(state, 'Unexpected character');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const number = currencyToAmount(numberStr);
|
|
72
|
+
if (number === null) {
|
|
73
|
+
fail(state, 'Invalid number format');
|
|
74
|
+
}
|
|
75
|
+
return isNegative ? -number : number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseParens(state: ParserState): AstNode {
|
|
79
|
+
if (char(state) === '(') {
|
|
80
|
+
next(state);
|
|
81
|
+
const expr = parseOperator(state);
|
|
82
|
+
|
|
83
|
+
if (char(state) !== ')') {
|
|
84
|
+
fail(state, 'Unbalanced parentheses');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
next(state);
|
|
88
|
+
return expr;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return parsePrimary(state);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseExponent(state: ParserState): AstNode {
|
|
95
|
+
let node = parseParens(state);
|
|
96
|
+
if (nextOperator(state, '^')) {
|
|
97
|
+
node = { op: '^', left: node, right: parseExponent(state) };
|
|
98
|
+
}
|
|
99
|
+
return node;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseMultiplicative(state: ParserState): AstNode {
|
|
103
|
+
let node = parseExponent(state);
|
|
104
|
+
while (char(state) === '*' || char(state) === '/') {
|
|
105
|
+
const op = next(state) as '*' | '/';
|
|
106
|
+
node = { op, left: node, right: parseExponent(state) };
|
|
107
|
+
}
|
|
108
|
+
return node;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseAdditive(state: ParserState): AstNode {
|
|
112
|
+
let node = parseMultiplicative(state);
|
|
113
|
+
while (char(state) === '+' || char(state) === '-') {
|
|
114
|
+
const op = next(state) as '+' | '-';
|
|
115
|
+
node = { op, left: node, right: parseMultiplicative(state) };
|
|
116
|
+
}
|
|
117
|
+
return node;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// These operators go from high to low order of precedence
|
|
121
|
+
const parseOperator = parseAdditive;
|
|
122
|
+
|
|
123
|
+
function parse(expression: string): AstNode {
|
|
124
|
+
const state = { str: expression.replace(/\s/g, ''), index: 0 };
|
|
125
|
+
return parseOperator(state);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function evaluate(ast: AstNode): number {
|
|
129
|
+
if (typeof ast === 'number') {
|
|
130
|
+
return ast;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { left, right, op } = ast;
|
|
134
|
+
|
|
135
|
+
switch (op) {
|
|
136
|
+
case '+':
|
|
137
|
+
return evaluate(left) + evaluate(right);
|
|
138
|
+
case '-':
|
|
139
|
+
return evaluate(left) - evaluate(right);
|
|
140
|
+
case '*':
|
|
141
|
+
return evaluate(left) * evaluate(right);
|
|
142
|
+
case '/':
|
|
143
|
+
return evaluate(left) / evaluate(right);
|
|
144
|
+
case '^':
|
|
145
|
+
return Math.pow(evaluate(left), evaluate(right));
|
|
146
|
+
default:
|
|
147
|
+
throw new Error('Unknown operator: ' + op);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function evalArithmetic(
|
|
152
|
+
expression: string,
|
|
153
|
+
defaultValue: number | null = null,
|
|
154
|
+
): number | null {
|
|
155
|
+
// An empty expression always evals to the default
|
|
156
|
+
if (expression === '') {
|
|
157
|
+
return defaultValue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let result: number;
|
|
161
|
+
try {
|
|
162
|
+
result = evaluate(parse(expression));
|
|
163
|
+
} catch {
|
|
164
|
+
// If it errors, return the default value
|
|
165
|
+
return defaultValue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Never return NaN
|
|
169
|
+
return isNaN(result) ? defaultValue : result;
|
|
170
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import { once, sequential } from './async';
|
|
3
|
+
|
|
4
|
+
function timeout(n) {
|
|
5
|
+
return new Promise(resolve => setTimeout(resolve, n));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function makeFunction(data) {
|
|
9
|
+
return async function fn(n, { throwError = false } = {}) {
|
|
10
|
+
data.push(n);
|
|
11
|
+
await timeout(10);
|
|
12
|
+
|
|
13
|
+
if (throwError) {
|
|
14
|
+
throw new Error('throwing error');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
data.push(n);
|
|
18
|
+
await timeout(50);
|
|
19
|
+
data.push(n);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('async', () => {
|
|
24
|
+
test('sequential fn should force concurrent calls to be in order', async () => {
|
|
25
|
+
const test = async fn => {
|
|
26
|
+
fn(1);
|
|
27
|
+
fn(2);
|
|
28
|
+
await fn(3);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const data = [];
|
|
32
|
+
await test(makeFunction(data));
|
|
33
|
+
expect(data).toEqual([1, 2, 3, 1, 2, 3, 1, 2, 3]);
|
|
34
|
+
|
|
35
|
+
const seqData = [];
|
|
36
|
+
await test(sequential(makeFunction(seqData)));
|
|
37
|
+
expect(seqData).toEqual([1, 1, 1, 2, 2, 2, 3, 3, 3]);
|
|
38
|
+
|
|
39
|
+
expect(data.length).toEqual(seqData.length);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('sequential fn should always call function when queue is empty', async () => {
|
|
43
|
+
const test = async fn => {
|
|
44
|
+
await fn(1);
|
|
45
|
+
await fn(2);
|
|
46
|
+
await fn(3);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const data = [];
|
|
50
|
+
await test(makeFunction(data));
|
|
51
|
+
expect(data).toEqual([1, 1, 1, 2, 2, 2, 3, 3, 3]);
|
|
52
|
+
|
|
53
|
+
const seqData = [];
|
|
54
|
+
await test(sequential(makeFunction(seqData)));
|
|
55
|
+
expect(seqData).toEqual([1, 1, 1, 2, 2, 2, 3, 3, 3]);
|
|
56
|
+
|
|
57
|
+
expect(data.length).toEqual(seqData.length);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('sequential fn should still flush queue when error is thrown', async () => {
|
|
61
|
+
const test = async fn => {
|
|
62
|
+
fn(1);
|
|
63
|
+
fn(2, { throwError: true }).catch(() => {
|
|
64
|
+
// Ignore errors
|
|
65
|
+
});
|
|
66
|
+
await fn(3);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const data = [];
|
|
70
|
+
await test(makeFunction(data));
|
|
71
|
+
expect(data).toEqual([1, 2, 3, 1, 3, 1, 3]);
|
|
72
|
+
|
|
73
|
+
const seqData = [];
|
|
74
|
+
await test(sequential(makeFunction(seqData)));
|
|
75
|
+
expect(seqData).toEqual([1, 1, 1, 2, 3, 3, 3]);
|
|
76
|
+
|
|
77
|
+
expect(data.length).toEqual(seqData.length);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('sequential fn should ignore promise chains in the future', async () => {
|
|
81
|
+
const data = [];
|
|
82
|
+
const fn = sequential(makeFunction(data));
|
|
83
|
+
|
|
84
|
+
void fn(1).then(() => {
|
|
85
|
+
// The next call should already have started (so it should have
|
|
86
|
+
// already appended 2 to the end). It shouldn't depend on this
|
|
87
|
+
// promise chain at all (important part being that if any errors
|
|
88
|
+
// happened in here, it wouldn't effect anything else)
|
|
89
|
+
expect(data).toEqual([1, 1, 1, 2]);
|
|
90
|
+
});
|
|
91
|
+
fn(2, { throwError: true }).catch(() => {
|
|
92
|
+
// Same as above
|
|
93
|
+
expect(data).toEqual([1, 1, 1, 2, 3]);
|
|
94
|
+
});
|
|
95
|
+
await fn(3);
|
|
96
|
+
|
|
97
|
+
expect(data).toEqual([1, 1, 1, 2, 3, 3, 3]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('once fn should only be called once', async () => {
|
|
101
|
+
let timesCalled = 0;
|
|
102
|
+
const fn = once(async () => {
|
|
103
|
+
await timeout(200);
|
|
104
|
+
timesCalled++;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await Promise.all([fn(), fn(), fn()]);
|
|
108
|
+
|
|
109
|
+
// It should only have been called once
|
|
110
|
+
expect(timesCalled).toBe(1);
|
|
111
|
+
|
|
112
|
+
// Make sure it's called again now that it's done executing
|
|
113
|
+
await Promise.all([fn(), fn()]);
|
|
114
|
+
expect(timesCalled).toBe(2);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('once fn should coalesce multiple calls', async () => {
|
|
118
|
+
let timesCalled = 0;
|
|
119
|
+
const fn = once(async () => {
|
|
120
|
+
await timeout(200);
|
|
121
|
+
timesCalled++;
|
|
122
|
+
return {};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const results = await Promise.all([fn(), fn(), fn()]);
|
|
126
|
+
|
|
127
|
+
// It should only have been called once
|
|
128
|
+
expect(timesCalled).toBe(1);
|
|
129
|
+
|
|
130
|
+
// The results should all be identical (`toBe` is a strict
|
|
131
|
+
// comparison, like ===)
|
|
132
|
+
expect(results[0]).toBe(results[1]);
|
|
133
|
+
expect(results[0]).toBe(results[2]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
2
|
+
type AnyFunction = (...args: any[]) => any;
|
|
3
|
+
|
|
4
|
+
export function sequential<T extends AnyFunction>(
|
|
5
|
+
fn: T,
|
|
6
|
+
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
|
|
7
|
+
const sequenceState: {
|
|
8
|
+
running: Promise<Awaited<ReturnType<T>>> | null;
|
|
9
|
+
queue: Array<{
|
|
10
|
+
args: Parameters<T>;
|
|
11
|
+
resolve: (
|
|
12
|
+
value: Awaited<ReturnType<T>> | PromiseLike<Awaited<ReturnType<T>>>,
|
|
13
|
+
) => void;
|
|
14
|
+
reject: (reason?: unknown) => void;
|
|
15
|
+
}>;
|
|
16
|
+
} = {
|
|
17
|
+
running: null,
|
|
18
|
+
queue: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function pump() {
|
|
22
|
+
const next = sequenceState.queue.shift();
|
|
23
|
+
if (next !== undefined) {
|
|
24
|
+
run(next.args, next.resolve, next.reject);
|
|
25
|
+
} else {
|
|
26
|
+
sequenceState.running = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run(
|
|
31
|
+
args: Parameters<T>,
|
|
32
|
+
resolve: (
|
|
33
|
+
value: Awaited<ReturnType<T>> | PromiseLike<Awaited<ReturnType<T>>>,
|
|
34
|
+
) => void,
|
|
35
|
+
reject: (reason?: unknown) => void,
|
|
36
|
+
) {
|
|
37
|
+
sequenceState.running = fn.apply(null, args).then(
|
|
38
|
+
(val: Awaited<ReturnType<T>> | PromiseLike<Awaited<ReturnType<T>>>) => {
|
|
39
|
+
pump();
|
|
40
|
+
resolve(val);
|
|
41
|
+
},
|
|
42
|
+
(err: unknown) => {
|
|
43
|
+
pump();
|
|
44
|
+
reject(err);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return ((...args: Parameters<T>) => {
|
|
50
|
+
if (!sequenceState.running) {
|
|
51
|
+
return new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
|
|
52
|
+
return run(args, resolve, reject);
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
return new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
|
|
56
|
+
sequenceState.queue.push({ resolve, reject, args });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}) as T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function once<T extends AnyFunction>(
|
|
63
|
+
fn: T,
|
|
64
|
+
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> | null {
|
|
65
|
+
let promise: Promise<Awaited<ReturnType<T>>> | null = null;
|
|
66
|
+
return (...args: Parameters<T>) => {
|
|
67
|
+
if (!promise) {
|
|
68
|
+
promise = fn.apply(null, args).finally(() => {
|
|
69
|
+
promise = null;
|
|
70
|
+
});
|
|
71
|
+
return promise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return promise;
|
|
75
|
+
};
|
|
76
|
+
}
|