IronFlock SDK
L’SDK IronFlock consente alle tue applicazioni edge di interagire con la piattaforma IronFlock. È disponibile in tre linguaggi — Python, JavaScript e Go — con le stesse funzionalità, documentate fianco a fianco qui sotto. Gestisce l’autenticazione automaticamente quando viene eseguito su un dispositivo registrato e fornisce funzioni per pubblicare dati, interrogare la cronologia, chiamare procedure remote tra dispositivi e aggiornare i metadati dei dispositivi.
| SDK | Pacchetto | Richiede |
|---|---|---|
| Python | ironflock su PyPI | Python 3.8+ |
| JavaScript | ironflock su npm | Node.js 18+ o browser moderno |
| Go | github.com/RecordEvolution/ironflock-go | Go 1.27.2+ |
Installazione
Python
pip install ironflockOppure aggiungi ironflock al file requirements.txt della tua app.
Avvio Rapido
Python
import asyncio
from datetime import datetime, timezone
from ironflock import IronFlock
async def main():
while True:
await ironflock.publish_to_table("sensordata", {
"tsp": datetime.now(timezone.utc).isoformat(),
"temperature": 22.5,
"humidity": 60
})
await asyncio.sleep(5)
ironflock = IronFlock(mainFunc=main)
ironflock.run()Quando viene usato all’interno di un container dell’app IronFlock, l’SDK legge automaticamente le credenziali di connessione dall’ambiente — nessuna configurazione manuale necessaria. Ogni riga che scrivi in una tabella porta un tsp, il suo timestamp (vedi publishToTable).
Opzioni del Costruttore
Python
ironflock = IronFlock(
mainFunc=main, # async function to run after connecting
serial_number="abc123", # override device serial (optional)
reconnect_window=60, # seconds a table operation rides out a platform restart (optional)
)| Parametro | Descrizione |
|---|---|
mainFunc | Una funzione asincrona che viene eseguita una volta stabilita la connessione |
serial_number | Sovrascrive il numero seriale del dispositivo inviato con le pubblicazioni e le scritture nelle tabelle. Predefinito: variabile d’ambiente DEVICE_SERIAL_NUMBER. Non cambia l’identità con cui la connessione si autentica finché l’agente del dispositivo inietta credenziali proprie dell’app |
reconnect_window | Secondi per cui un’operazione su tabella attende il ritorno della piattaforma dopo un riavvio prima di sollevare un errore. Predefinito: 60; 0 lo disattiva (vedi Affidabilità della Connessione) |
Pubblicazione dei Dati
publishToTable / publish_to_table
Pubblica un record di dati in una tabella della flotta. Il nome della tabella deve corrispondere a una tabella definita nel file data-template.yml della tua app. L’SDK instrada automaticamente i dati al database del progetto corretto.
Python
from datetime import datetime, timezone
await ironflock.publish_to_table("sensordata", {
"tsp": datetime.now(timezone.utc).isoformat(),
"temperature": 22.5,
"humidity": 60,
"device_id": "sensor-001"
})Il data backend legge la riga dal primo argomento: un dict in Python, il primo elemento dell’array in JavaScript, il primo argomento posizionale in Go (una map o una struct). Gli argomenti con parola chiave aggiuntivi — in Go un valore ironflock.Kwargs tra gli argomenti — vengono letti solo per le colonne a cui il data template li associa (kwargs.<name>).
Ogni riga ha bisogno di un tsp. Ogni tabella ha una colonna tsp obbligatoria, il timestamp della riga — ad es. una stringa ISO 8601, o in Go un time.Time, che viene inviato come RFC 3339 in UTC — che il data backend legge dalla riga, a meno che il data template non la associ a un’altra fonte. Il data backend scarta una riga pubblicata senza tsp e ne lascia traccia solo con una voce nella tabella error-logs dell’app — la pubblicazione confermata sembra riuscita. Una riga rifiutata per mancanza di spazio di archiviazione viene scartata senza nemmeno quella. Usa appendToTable / append_to_table quando devi sapere se una riga è stata memorizzata.
Righe parziali nelle tabelle entità. In una tabella con una chiave di entità (maintainLatestFlagFor), una riga può contenere soltanto la chiave di entità, tsp e le colonne che cambiano. Una colonna che la riga omette mantiene il valore precedente dell’entità; una colonna inviata come None / null / nil viene svuotata. Se un campo conta come omesso dipende da come costruisci la riga:
- Python: una chiave con valore
Noneviene inviata come null e svuota la colonna. Rimuovi la chiave dal dict per mantenere il valore precedente. - JavaScript: una proprietà con valore
undefinedviene scartata dal payload, quindi mantiene il valore precedente. Impostala anullper svuotare la colonna. - Go: un campo di struct con il tag
omitemptyoomitzeroviene omesso quando è vuoto, quindi mantiene il valore precedente. Altrimenti un campo puntatorenil, come un valorenilin unironflock.Row, viene inviato come null e svuota la colonna.
Lo stesso vale per tutti e quattro i metodi di scrittura. In una tabella senza chiave di entità, una colonna che la riga omette viene memorizzata come NULL.
appendToTable / append_to_table
Aggiunge dati a una tabella della flotta tramite una chiamata di procedura remota invece di pub/sub. Usalo quando hai bisogno della conferma che i dati siano stati persistiti: una riga rifiutata è un errore.
Python
result = await ironflock.append_to_table("sensordata", {
"tsp": datetime.now(timezone.utc).isoformat(),
"temperature": 22.5,
"humidity": 60
})La riga ha bisogno di un tsp, come per publishToTable. Il data backend rifiuta una riga senza tsp con wamp.error.runtime_error e senza motivo; l’errore dell’SDK aggiunge allora “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. I rifiuti tipizzati, come un disco pieno su un’appliance, sono elencati in Operazioni rifiutate dal data backend.
publishRowsToTable / publish_rows_to_table
Pubblica molte righe in un singolo messaggio (inserimento in blocco) in una tabella della flotta. La piattaforma inserisce l’intero batch in modo atomico (tutto-o-niente) in un’unica operazione. Usalo per dati ad alta frequenza dove un round-trip per riga sarebbe troppo costoso. Come publishToTable, è fire-and-forget — la conferma attesta la consegna al router, non l’inserimento nel database.
Python
await ironflock.publish_rows_to_table("sensordata", [
{"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5},
{"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7},
])Il secondo argomento è una lista non vuota di oggetti riga da inserire, ognuno con il proprio tsp — in Go una slice di righe ([]ironflock.Row, []map[string]any o una slice di struct). Un batch che il data backend rifiuta — perché a una riga manca il suo tsp, ad esempio — viene scartato senza che chi pubblica ne sappia nulla, come con publishToTable.
appendRowsToTable / append_rows_to_table
Aggiunge molte righe in una singola chiamata di procedura remota (inserimento in blocco) a una tabella della flotta. La piattaforma inserisce l’intero batch in modo atomico (tutto-o-niente): se una qualsiasi riga non è valida, l’intero batch viene rifiutato e nulla viene persistito. Preferiscilo a publishRowsToTable / publish_rows_to_table quando hai bisogno dell’esito dell’inserimento. Ogni riga ha bisogno del proprio tsp; i rifiuti sono quelli di appendToTable, incluso il suggerimento per un batch rifiutato senza motivo.
Python
result = await ironflock.append_rows_to_table("sensordata", [
{"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5},
{"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7},
])
# result -> {"success": True, "count": 2}reportError / report_error
Segnala un errore dell’applicazione nella tabella error-logs della tua flotta. Si tratta di un wrapper di comodità su publishToTable / appendToTable: aggiunge alla riga source: "app", un level di gravità e un timestamp, quindi la scrive come qualsiasi normale riga di tabella. L’errore finisce nella stessa tabella error-logs usata dagli errori di sistema di fleetdb (contrassegnati con source: "system"), perciò è interrogabile con getHistory, trasmissibile in streaming con subscribeToTable / subscribe_to_table, utilizzabile nei board-templates e consegnato in tempo reale su transformed.error-logs — senza far comparire il toast di errore di sistema della piattaforma.
Python
# Fire-and-forget (default): publishes to the error-logs table
await ironflock.report_error("Sensor read timed out", level="warn")
# Pass an exception to capture its traceback (falls back to the message)
try:
risky_operation()
except Exception as err:
await ironflock.report_error(err)
# Use the append RPC when you want to await the insert outcome, and show the
# operator a friendlier line than the raw error
await ironflock.report_error(
"Calibration failed: offset 0.42 out of range",
append=True,
user_message="Please recalibrate the sensor",
)La riga è { tsp, msg, user_message, source: "app", level }: msg contiene il testo tecnico (un traceback o uno stack), user_message la riga che mostra il toast di errore del board — il messaggio utente che passi oppure, in sua assenza, il messaggio stesso.
Parametri:
| Parametro | Tipo | Descrizione |
|---|---|---|
error | str / string o eccezione / Error | Il messaggio di errore, oppure un’eccezione il cui traceback/stack (o messaggio) viene registrato |
level | str / string, opzionale | Gravità: "error", "warn", "info" o "debug". Il valore predefinito è "error" |
append | bool / boolean, opzionale | Quando è true, usa la procedura remota di append (restituisce l’esito dell’inserimento). Il valore predefinito è false (pubblicazione fire-and-forget) |
tsp | str / string, opzionale | Sovrascrittura del timestamp ISO-8601. Il valore predefinito è l’ora corrente |
user_message / userMessage | str / string, opzionale | Testo destinato all’operatore, memorizzato come user_message. Il valore predefinito è il messaggio registrato |
In Python le opzioni sono argomenti con parola chiave (
report_error(error, level=..., append=..., tsp=..., user_message=...)); in JavaScript vengono passate tramite un oggetto di opzioni (reportError(error, { level, append, tsp, userMessage })); in Go tramite una struct (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), doveerrOrMsgè unerroro una stringa,Leveluno traironflock.LevelError,LevelWarn,LevelInfo,LevelDebug, e il*Resultrestituito ènila meno che non sia impostatoAppend.
publish
Pubblica un messaggio su un tuo topic WAMP. Usalo per messaggi personalizzati o eventi che non si mappano su una tabella del database. Il router riserva alcuni nomi alla piattaforma — vedi URI Consentiti a un’App.
Python
await ironflock.publish("com.myapp.alerts", {
"level": "warning",
"message": "Temperature threshold exceeded"
})Interrogazione dei Dati Storici
getHistory
Recupera dati storici da una tabella della flotta o da un transform: le righe più recenti che corrispondono, in ordine crescente di tsp. Supporta filtraggio, intervalli di tempo e paginazione.
Python
# Simple query
data = await ironflock.getHistory("sensordata", {"limit": 100})
# Query with time range and filters
data = await ironflock.getHistory("sensordata", {
"limit": 500,
"offset": 0,
"timeRange": ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"],
"filterAnd": [
{"column": "temperature", "operator": ">", "value": 20},
{"column": "humidity", "operator": "<=", "value": 80}
]
})
# Current value(s) only: the "latest" marker returns the newest row per entity
current = await ironflock.getHistory("sensordata", {
"limit": 100,
"filterAnd": [{"latest": True}]
})
# One field of a large json column: the column comes back pruned to its paths
readings = await ironflock.getHistory("sensordata", {
"limit": 100,
"columns": ["json_data"],
"columnPaths": ["json_data.sensor.temp"],
})Parametri di query:
| Campo | Tipo | Descrizione |
|---|---|---|
limit | int / number | Numero massimo di righe da restituire (1–10.000, obbligatorio; 1–3.000 per un transform) |
offset | int / number | Offset per la paginazione |
timeRange | list / array | [start, end] — stringhe data-ora ISO o numeri in millisecondi epoch, mai mescolati; None / null per un estremo aperto. Un oggetto legacy {"start": ..., "end": ...} viene convertito nella coppia |
filterAnd | list / array | Voci di filtro combinate in AND: condizioni, gruppi e/o il marcatore latest (vedi sotto) |
columns | list / array | Colonne da restituire (opzionale). tsp, device_key e authid sono sempre incluse; ometti per avere tutte le colonne |
columnPaths | list / array | Riduce le colonne json (opzionale): percorsi JSON nella notazione usata dai filtri ("json_data.a.b", "json_data['key']"). Una colonna json elencata in columns torna contenendo solo l’unione dei suoi percorsi; una senza percorso qui torna intera. Ignorato dai transform e dai data backend precedenti a fleetdb v1.0.52 |
In Go questi sono i campi Limit, Offset, TimeRange, FilterAnd, Columns e ColumnPaths di ironflock.TableQueryParams. TimeRange è un *ironflock.TimeRange{Start, End}: ogni estremo è un time.Time, una stringa ISO 8601, un numero in millisecondi epoch oppure nil per un estremo aperto (ironflock.Between, Since e Until ne costruiscono uno a partire da valori time.Time). FilterAnd è un []ironflock.Filter. La forma legacy a oggetto non esiste.
Operatori di filtro: =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL
Ogni condizione di filtro è un oggetto con le chiavi column, operator e value — in Go, costruito con ironflock.Where(column, operator, value), ironflock.IsNull(column) e ironflock.IsNotNull(column). Gli operatori vengono riconosciuti senza distinzione tra maiuscole e minuscole, ignorando gli spazi circostanti, e inviati nella forma elencata sopra — l’unica che il data backend accetta —, quindi "like" viene inviato come "LIKE", anche all’interno dei gruppi. Un operatore fuori dall’elenco viene inviato così com’è, con un avviso. IS NULL e IS NOT NULL non accettano value e sono l’unico modo per interrogare su NULL: qualsiasi altro operatore esclude una riga la cui colonna è NULL, esattamente come in SQL. IN e NOT IN accettano una lista (una slice in Go). LIKE distingue tra maiuscole e minuscole; usa ILIKE per un confronto che non le distingue.
Combinare con OR. Le voci di filterAnd sono combinate in AND. Una voce di gruppo combina le proprie voci — condizioni, il marcatore latest o altri gruppi — con un unico operatore, "AND" o "OR":
Python
# Rows that are not soft-deleted: deleted IS NULL OR deleted = false
"filterAnd": [
{
"combinator": "OR",
"filters": [
{"column": "deleted", "operator": "IS NULL"},
{"column": "deleted", "operator": "=", "value": False},
],
}
]Un data backend che precede i gruppi (fleetdb prima della v1.2.0) rifiuta questa forma invece di applicarne solo una parte, così fallisce in modo esplicito invece di restituire silenziosamente righe che il filtro esclude.
Leggere i valori correnti. Una voce {"latest": true} in filterAnd (ironflock.Latest() in Go) non è una condizione di filtro ma un cambio di modalità: il Data Backend restituisce solo la riga più recente per ogni entità, derivata in SQL dalla chiave di entità che la tabella dichiara con maintainLatestFlagFor. Una tabella senza chiave di entità restituisce la sua singola riga più recente.
Le altre condizioni si combinano con il marcatore come ci si aspetta: le condizioni sulle colonne della chiave di entità restringono quali entità vengono restituite, mentre tutte le altre condizioni e timeRange vengono applicate alle righe più recenti risultanti. Quindi combinare {"latest": true} con un filtro su deleted nasconde le entità eliminate invece di far riemergere la loro riga precedente.
Le versioni precedenti di IronFlock memorizzavano una colonna fisica latest_flag. Non esiste più — un filtro legacy latest_flag = true viene ancora accettato e trattato come il marcatore, ma il codice nuovo dovrebbe usare {"latest": true}. Il marcatore latest non è disponibile in getSeriesHistory.
Intervalli di tempo. Il data backend legge un estremo in forma di stringa con il Date di JavaScript, quindi scrivi una data o una data-ora ISO 8601 (2026-07-01, 2026-07-01T12:30:15.250Z) e separa data e ora con T. Gli SDK rifiutano uno spazio tra data e ora negli anni 0–99, che Date interpreta male: "0001-01-01 00:00:00" — ciò che restituisce str(datetime.min) in Python — verrebbe letto come 2001. Un’ora senza fuso orario viene letta come UTC. Un estremo numerico è espresso in millisecondi epoch e deve essere finito: NaN e ±Infinity arriverebbero al data backend come estremo aperto e farebbero cadere silenziosamente il filtro temporale, quindi gli SDK li rifiutano prima dell’invio. In Go, un estremo time.Time viene inviato come RFC 3339 in UTC, e un estremo stringa deve essere una delle forme ISO 8601 che Date legge correttamente — una data, oppure una data e un’ora del giorno con una Z o uno scostamento da UTC facoltativi (+02:00 o +0200); le altre stringhe, come il formato base 20260701T123000Z, uno scostamento di sole ore o un giorno che il mese non ha (2026-02-30), falliscono con ironflock.ErrInvalidArgument prima che venga inviato qualcosa.
Transform. Un transform (una vista SQL di un data template, vedi Tabelle di Trasformazione) si legge in modo diverso da una tabella. Il data backend applica solo limit — al massimo 3000, un valore maggiore viene rifiutato con URI_INVALID_LIMIT —, offset e filterAnd. Ignora timeRange, columns, columnPaths e il marcatore latest, e scarta un filtro su una colonna che il transform non dichiara (un transform non ha le colonne implicite tsp, device_key o authid), il che allarga il risultato. Le sue righe arrivano nell’ordine proprio della vista, invertito: ottieni le righe più recenti, dalla più vecchia alla più recente, solo quando la vista ordina per prime le più recenti. Delimita l’intervallo di tempo di un transform nel suo SQL.
Risultati di grandi dimensioni. Un risultato troppo grande per un singolo messaggio del data backend (8 MiB di JSON) arriva in chunk, che l’SDK riassembla in memoria: ottieni le righe di una singola lettura, nello stesso ordine. Chunk che non tornano — uno mancante o duplicato, o un numero di righe diverso dal riepilogo del data backend — fanno fallire la lettura invece di restituire una parte delle righe. Una singola riga oltre 8 MiB, o un risultato oltre la soglia di protezione di 1 GiB del data backend, viene rifiutato con URI_RESULT_TOO_LARGE: abbassa limit, restringi timeRange, seleziona meno columns o riduci con columnPaths.
getSeriesHistory / get_series_history
Recupera dati di serie temporali sotto-campionati da una tabella di flotta: colonne aggregate per intervallo di tempo (ad es. medie orarie). Ideale per grafici su intervalli di tempo lunghi. Disponibile per le tabelle (non per i transform), sui data backend a partire da fleetdb v1.0.58.
Python
from datetime import datetime, timedelta, timezone
from ironflock import SeriesMetric
now = datetime.now(timezone.utc)
avg = SeriesMetric(ref="temperature", method="AVG") # AVG, SUM, COUNT, MIN, MAX, FIRST, LAST
series = await ironflock.get_series_history("sensordata", {
"metrics": [avg, {"ref": "humidity", "method": "MAX"}],
"bucketMs": 3_600_000, # one-hour buckets; omit to divide the range into `limit` buckets
"limit": 24, # the bucket budget, 1–10000
"timeRange": [(now - timedelta(hours=24)).isoformat(), now.isoformat()], # required, with a start
"groupBy": ["device_key"],
})
for row in series:
print(row["tsp"], row["device_key"], row[avg.column]) # avg.column is "AVG:temperature"Parametri di query:
| Campo | Tipo | Descrizione |
|---|---|---|
metrics | list / array | Gli aggregati calcolati per intervallo, almeno uno: {"ref": ..., "method": ...}. ref è una colonna, o un percorso JSON in una colonna json ("json_data.temp"); method è "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" o "LAST". Ogni metrica torna sotto "<METHOD>:<ref>", che deve stare in 63 byte |
bucketMs | int / number | Ampiezza dell’intervallo in millisecondi interi, almeno 1000 (opzionale). Omettilo per dividere l’intervallo di tempo in limit intervalli |
limit | int / number | Il budget di intervalli (1–10.000) — non un limite di righe |
timeRange | list / array | [start, end] — stringhe data-ora ISO o numeri epoch-ms. Obbligatorio, con un inizio; None / null come fine significa adesso. La fine deve essere successiva all’inizio |
groupBy | list / array | Colonne che suddividono ogni intervallo in una riga per gruppo (opzionale) |
filterAnd | list / array | Condizioni e gruppi combinati in AND, come per getHistory (opzionale). Il marcatore latest qui viene rifiutato, anche all’interno di un gruppo; usa getHistory per leggere i valori correnti |
Righe. Ogni riga è un intervallo — di un gruppo, con groupBy. tsp è l’inizio dell’intervallo in millisecondi epoch, ogni colonna di groupBy torna con il proprio nome e ogni metrica sotto "<METHOD>:<ref>" (SeriesMetric.column in Python, seriesColumn(metric) in JavaScript, SeriesMetric.Column() in Go), ad es. row["AVG:temperature"]. Le righe sono in ordine crescente di tsp; un intervallo senza righe è assente (nessun riempimento dei buchi). AVG e SUM accettano colonne numeriche, MIN e MAX anche stringhe e timestamp; FIRST e LAST danno il valore della riga più vecchia e di quella più recente dell’intervallo; COUNT conta i valori non nulli (per tsp: le righe). Un risultato di grandi dimensioni viene riassemblato dai chunk, come per getHistory.
Intervalli e un estremo aperto. limit conta intervalli, non righe. Senza bucketMs l’intervallo di tempo viene diviso in limit intervalli. Con bucketMs, il data backend allarga gli intervalli a un multiplo di bucketMs quando l’intervallo di tempo ne contiene più di limit — e una fine aperta arriva fino all’adesso del data backend, un po’ dopo il tuo. Le ultime 24 ore con una fine aperta contengono 25 intervalli di un’ora, quindi limit: 24 ne ottiene di due ore. Delimita la fine, come nell’esempio, oppure concedi un intervallo in più: con bucketMs e una fine aperta possono tornare limit + 1 intervalli.
Gli SDK verificano prima dell’invio ciò che il data backend rifiuterebbe: almeno una metrica, ref non vuoti, metodi noti, nomi di colonna entro 63 byte, un inizio, una fine successiva all’inizio e bucketMs come intero di almeno 1000 (in Go, Bucket di almeno 1 secondo, in millisecondi interi). Il data backend rifiuta ciò a cui non può rispondere con URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY o URI_SERIES_TOO_MANY_GROUPS (più di 50.000 righe) — vedi Operazioni rifiutate dal data backend.
Forma deprecata. Le versioni dell’SDK fino alla 1.9.0 inviavano
metricscome nomi di colonna più unmethoddi primo livello ({"metrics": ["temperature"], "method": "AVG", ...}), che i data backend rifiutano da fleetdb v1.0.58. Quella forma funziona ancora: l’SDK invia una metrica{ref, method}per colonna, emette un avviso (unDeprecationWarningin Python, un avviso nella console una volta per processo in JavaScript) e rinomina le colonne del risultato con i nomi di colonna, così il vecchio codice continua a leggererow["temperature"]— a meno che la riga non abbia già una colonna con quel nome, cometspperCOUNTditsp. Passa alle metriche{ref, method}e leggirow["AVG:temperature"]. L’SDK Go ha solo la forma attuale: un metodo per metrica (SeriesMetric).
Sottoscrizione ai Dati
subscribeToTable / subscribe_to_table
Sottoscrive agli aggiornamenti in tempo reale di una tabella della flotta. Il gestore viene chiamato ogni volta che il data backend memorizza una nuova riga, con la riga così come è stata memorizzata — valori tipizzati secondo le colonne del data template, colonne segrete mascherate — come primo argomento. Le righe scritte tramite il percorso di inserimento in blocco (publishRowsToTable / appendRowsToTable) vengono consegnate al tuo gestore una alla volta, quindi il codice del gestore rimane lo stesso indipendentemente da come i dati sono stati scritti.
Python
def on_sensor_data(*args, **kwargs):
row = args[0]
print("New reading:", row["tsp"], row["temperature"])
await ironflock.subscribe_to_table("sensordata", on_sensor_data)Un transform non ha righe proprie: a ogni esecuzione della sua pianificazione il data backend pubblica l’intera vista — al massimo 3000 righe, nell’ordine inverso della vista — come un unico evento il cui primo argomento è la lista delle righe. event_rows(args[0]) in Python, eventRows(args) in JavaScript e ev.Rows() in Go leggono entrambe le forme: la riga di una tabella dà [row], la lista di un transform la lista stessa ([] per una vista vuota), qualsiasi altra cosa None / undefined / nil. In Go, ev.Row() è nil per l’evento di un transform.
Python
from ironflock import event_rows
def on_hourly(*args, **kwargs):
for row in event_rows(args[0]) or []:
print(row)
await ironflock.subscribe_to_table("hourly_averages", on_hourly)subscribe
Sottoscrive a un tuo topic WAMP per la messaggistica in tempo reale personalizzata. Per le righe delle tabelle usa subscribeToTable / subscribe_to_table: il router rifiuta le sottoscrizioni ai topic grezzi di scrittura delle tabelle (vedi URI Consentiti a un’App).
Python
def on_alert(*args, **kwargs):
print("Alert received:", args, kwargs)
await ironflock.subscribe("com.myapp.alerts", on_alert)Colonne Segrete
Una colonna dichiarata secret: true nel data template dell’app viene cifrata a riposo. Nessun percorso di lettura ordinario restituisce il valore in chiaro, nemmeno all’app che lo ha scritto:
| Percorso di lettura | Come appare una colonna segreta |
|---|---|
getHistory, subscribeToTable / subscribe_to_table e tutto il resto attraverso il router dei messaggi | il segnaposto __secret__, esportato come SECRET_PLACEHOLDER (ironflock.SecretPlaceholder in Go) |
| Accesso SQL (il login Postgres di FleetDB Access) | il testo cifrato memorizzato, ifsec:1:… |
revealSecrets / reveal_secrets | il valore decifrato in chiaro |
NULL viene propagato come NULL su ogni percorso, così “impostato ma nascosto” rimane distinguibile da “mai scritto”.
Su una tabella entità, il ciclo lettura-modifica-scrittura è sicuro: ripubblicare una riga che contiene ancora il segnaposto __secret__ — oppure omettere del tutto la colonna segreta — mantiene il segreto memorizzato invece di sovrascriverlo, e un null esplicito lo cancella. Consulta Colonne Segrete nella guida al Data Backend per le regole esatte.
Python
from datetime import datetime, timezone
# Written like any other column — the data backend encrypts it on insert
await ironflock.publish_to_table("credentials", {
"tsp": datetime.now(timezone.utc).isoformat(),
"device_id": "d-1",
"api_token": "hunter2",
})
# A normal read redacts it
rows = await ironflock.getHistory("credentials", {"limit": 1})
# [{"tsp": "...", "device_id": "d-1", "api_token": "__secret__"}]Entrambe le funzioni descritte di seguito sono richiamabili solo dai container dell’app stessa. Il router dei messaggi le nega a ogni ruolo browser e alle app consumatrici, ed è per questo che un handle ConsumedApp non ha un equivalente: le colonne segrete di un’altra app arrivano oscurate e tali restano.
revealSecrets / reveal_secrets
Legge le righe di una delle tue tabelle con le colonne segrete decifrate.
Python
rows = await ironflock.reveal_secrets("credentials", {
"limit": 1,
"filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}],
})
# [{"tsp": "...", "device_id": "d-1", "api_token": "hunter2"}]I parametri di query sono gli stessi di getHistory, con una differenza: limit non può superare 100. Questo tetto esiste per impedire che una singola chiamata decifri un’intera tabella, e il data backend rifiuta un valore superiore invece di ridurlo silenziosamente.
Il data backend risponde al massimo a 30 rivelazioni al minuto per credenziale dell’app su un dispositivo; oltre, la chiamata fallisce con URI_RATE_LIMITED. Rivela un segreto una volta e conservalo, invece di farlo a ogni utilizzo. Una tabella senza colonne segrete viene rifiutata con URI_NOT_A_SECRET_COLUMN, un filtro su una colonna segreta con URI_SECRET_COLUMN.
verifySecret / verify_secret
Verifica un valore candidato rispetto a una colonna segreta senza rivelarla — la controparte “verifica della password” della funzione di rivelazione. Il confronto viene eseguito all’interno del data backend, in tempo costante, e torna indietro soltanto la risposta.
Python
ok = await ironflock.verify_secret(
"credentials", "api_token", submitted_token,
{"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]},
)
if ok:
...Parametri:
| Parametro | Tipo | Descrizione |
|---|---|---|
tablename | str / string | La tabella su cui effettuare la verifica |
column | str / string | La colonna segreta da confrontare |
candidate | str / string | Il valore in chiaro da verificare. Viene confrontato lato server e non viene mai scritto nella tabella |
query_params / queryParams | SecretQueryParams | Quali righe partecipano al confronto. Per impostazione predefinita la riga più recente; limit non può superare 100 |
In Go i parametri sono stringhe posizionali dopo il context, e la query è un *ironflock.TableQueryParams (nil per la riga più recente).
JavaScript restituisce { match, checked } e Go un *ironflock.SecretVerifyResult con Match e Checked; Python restituisce un semplice bool. Dove hai checked, vale la pena usarlo: distingue un candidato sbagliato (checked > 0) da un selettore che non ha trovato alcuna riga (checked === 0), due errori molto diversi da segnalare all’utente.
Poiché ogni riga è cifrata con un proprio valore casuale, due righe che contengono lo stesso segreto memorizzano testi cifrati diversi. È per questo che un filtro = su una colonna segreta non può funzionare — il data backend rifiuta qualsiasi filtro su una colonna di questo tipo con URI_SECRET_COLUMN — ed esiste invece questa funzione.
Il data backend risponde al massimo a 120 verifiche al minuto per credenziale dell’app su un dispositivo; oltre, la chiamata fallisce con URI_RATE_LIMITED. Per una verifica a ogni richiesta (una chiave API, ad esempio), memorizza brevemente nella cache un esito positivo. Una colonna che non è una colonna segreta viene rifiutata con URI_NOT_A_SECRET_COLUMN.
Accesso ai Dati tra App
Leggi i dati di flotta di un’altra app dall’interno della tua app, all’interno dello stesso progetto. La tua app deve dichiarare l’app fornitrice nella sezione consumes: del proprio data-template.yml (oppure possedere l’autorizzazione wildcard, vedi sotto), e l’utente del progetto deve concedere l’accesso. L’accesso è di sola lettura: puoi interrogare lo storico e sottoscrivere in tempo reale le righe delle tabelle e dei transform condivisi dal fornitore, ma non puoi scrivervi. Le connessioni alle app consumate vengono memorizzate nella cache per app e chiuse automaticamente quando la tua istanza si arresta.
La connessione richiede le credenziali proprie dell’app che l’agente del dispositivo inietta (APP_AUTH_ID, APP_AUTH_SECRET). La piattaforma rifiuta la vecchia credenziale del dispositivo — il ripiego sul numero seriale di un’app che non ha ricevuto credenziali proprie, ad es. con un agente del dispositivo meno recente — sul realm di un’altra app (NOT_AUTHORIZED, con un messaggio che lo indica), a meno che l’app fornitrice non sia in esecuzione sullo stesso dispositivo: in quel caso la connessione entra nel suo realm con i pieni diritti di quell’app anziché in sola lettura.
Se la tua app possiede l’autorizzazione wildcard (consumes: [{ app: "*" }]), puoi scoprire e aprire dinamicamente i fornitori con listConsumableApps / list_consumable_apps e connectToAllApps / connect_to_all_apps (di seguito).
connectToApp / connect_to_app
Apre una connessione di sola lettura al data backend di un’altra app e restituisce un handle. L’handle espone getHistory / get_history, subscribeToTable / subscribe_to_table e getSeriesHistory / get_series_history (solo tabelle) — le stesse interrogazioni e sottoscrizioni che usi sulle tue tabelle, con gli stessi parametri e risultati — oltre a close e ai cataloghi condivisi tables / transforms.
Python
# Open a read-only handle on another app's data backend
weather = await ironflock.connect_to_app("weather-app")
# Inspect what the provider shares
print([t["tablename"] for t in weather.tables])
# Query history and subscribe, just like your own tables
rows = await weather.get_history("forecasts", {"limit": 100})
def on_forecast(*args, **kwargs):
print("New forecast:", args)
await weather.subscribe_to_table("forecasts", on_forecast)Parametri:
| Parametro | Tipo | Descrizione |
|---|---|---|
app_name / appName | str / string | Nome dell’app fornitrice, come dichiarato nella tua sezione consumes: |
stage | str / string, opzionale | Stage del fornitore: "dev" o "prod". Il valore predefinito è lo stage della tua app |
on_error / onError | callable, opzionale | Viene invocato con un CrossAppAccessError quando l’accesso viene negato definitivamente alla connessione aperta (ad es. se l’autorizzazione viene revocata in seguito — vedi sotto quando un diniego conta come definitivo) |
Se l’accesso viene negato o usato in modo improprio, viene sollevato (Python) / lanciato (JavaScript) / restituito (Go, un *ironflock.CrossAppAccessError da verificare con errors.As) un CrossAppAccessError con un campo code (Code in Go, con costanti come ironflock.CodeNoGrant): NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (una query che filtra — anche all’interno di un gruppo — o seleziona una colonna che il fornitore contrassegna come segreta) o NOT_AUTHORIZED (l’autorizzazione è stata revocata o non è stato possibile verificarla in quel momento — oppure la tua app ha presentato la vecchia credenziale del dispositivo, nel qual caso il messaggio lo indica).
Un diniego mentre connectToApp / connect_to_app attende fa fallire subito la chiamata. Una volta aperto un handle, un diniego conta come definitivo solo se persiste — 3 o più rifiuti dall’ultima volta in cui la connessione era attiva (la chiusura della sessione da parte del router per un motivo di autenticazione conta come uno, così come ogni riconnessione rifiutata), il primo almeno 60 secondi fa —, perché la piattaforma rifiuta allo stesso modo quando per breve tempo non riesce a verificare l’accesso. Fino ad allora l’handle continua a riconnettersi e si ristabilisce se una riconnessione riesce. Una volta che il diniego è definitivo, viene chiamato on_error / onError / OnError e l’handle viene rimosso: il successivo connectToApp / connect_to_app / ConnectToApp apre una nuova connessione. Da quel momento le letture e le sottoscrizioni dell’handle falliscono subito, con l’errore ricevuto da on_error / onError — in Go, con un errore che avvolge wamp.ErrStopped.
In Python,
stageeon_errorsono argomenti con parola chiave; in JavaScript vengono passati tramite un oggetto di opzioni (connectToApp(appName, { stage, onError })); in Go tramite una struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), doveOnErrorè unafunc(*ironflock.CrossAppAccessError). In Go,ctxlimita solo l’attesa del chiamante: le chiamate concorrenti per la stessa app condividono un unico tentativo, eClose(ctx)chiude un handle.
listConsumableApps / list_consumable_apps
Elenca tutti i fornitori non privati nel progetto — la primitiva di scoperta per le app che possiedono l’autorizzazione di consumo wildcard (consumes: [{ app: "*" }] nel tuo data-template.yml, concessa dall’utente del progetto). Esegue una singola chiamata e non apre nessuna connessione: mostra i cataloghi restituiti in un selettore, quindi chiama connectToApp / connect_to_app per quelli che desideri — oppure connectToAllApps / connect_to_all_apps per aprirli tutti in una volta.
Nota: Dichiara l’autorizzazione nel
data-template.ymldella tua app, e metti tra virgolette il*— un*da solo è un alias YAML e non verrà interpretato:
consumes:
- app: "*"Python
providers = await ironflock.list_consumable_apps()
for p in providers:
print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]Ogni voce descrive un fornitore:
| Campo | Tipo | Descrizione |
|---|---|---|
app | str / string | Nome dell’app fornitrice |
provider_app_key | int / number | La chiave dell’app del fornitore |
stages | dict / object | Catalogo per stage { dev?, prod? }; uno stage è presente solo se il fornitore dispone di un data backend per esso. Ogni catalogo contiene le tables e i transforms non privati che condivide |
In Go ogni voce è un ironflock.ConsumedAppInfo con App, ProviderAppKey e Stages.Dev / Stages.Prod (un *StageCatalog con Tables e Transforms, nil per uno stage mancante); Catalog(stage) ne restituisce uno per nome.
Viene sollevato (Python) / lanciato (JavaScript) / restituito (Go) un CrossAppAccessError con code: NO_GRANT se la tua app non possiede alcuna autorizzazione wildcard.
connectToAllApps / connect_to_all_apps
Apre handle di sola lettura verso tutti i fornitori non privati nel progetto in un’unica chiamata (solo per i consumatori wildcard). Enumera i fornitori tramite listConsumableApps / list_consumable_apps e apre ciascuno di essi, saltando quelli senza un data backend per lo stage richiesto. Ogni handle viene memorizzato nella cache con la stessa chiave di connectToApp / connect_to_app, così una successiva chiamata a connectToApp(name) restituisce l’handle già pronto. Gli handle restituiti vengono chiusi insieme quando la tua istanza si arresta.
Python
apps = await ironflock.connect_to_all_apps(
on_error=lambda err: print("Provider skipped:", err)
)
for app in apps:
rows = await app.get_history(app.tables[0]["tablename"], {"limit": 10})
print(app.app, rows)Parametri:
| Parametro | Tipo | Descrizione |
|---|---|---|
stage | str / string, opzionale | Stage del fornitore: "dev" o "prod". Il valore predefinito è lo stage della tua app |
continue_on_error / continueOnError | bool / boolean, opzionale | Quando è true (il valore predefinito), un fornitore che non riesce ad aprirsi viene segnalato a on_error / onError e omesso dal risultato. Quando è false, il primo errore viene sollevato/lanciato |
on_error / onError | callable, opzionale | Viene invocato con ogni fornitore che non è stato possibile aprire (mentre continue_on_error / continueOnError è true), e con un CrossAppAccessError se a una connessione già aperta l’accesso viene successivamente negato in modo definitivo (ad es. se l’autorizzazione viene revocata) |
Restituisce gli handle dei fornitori aperti con successo (stesso tipo di handle di connectToApp / connect_to_app). Viene sollevato (Python) / lanciato (JavaScript) / restituito (Go) un CrossAppAccessError con code: NO_GRANT se la tua app non possiede alcuna autorizzazione wildcard.
In Python,
stage,on_errorecontinue_on_errorsono argomenti con parola chiave; in JavaScript vengono passati tramite un oggetto di opzioni (connectToAllApps({ stage, onError, continueOnError })); in Go tramite una struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), doveOnErrorè unafunc(error)eStopOnError: truecorrisponde acontinueOnError: false.
Archiviazione Gestita dei File
Ogni data backend di un’app dispone di un’archiviazione a oggetti privata accanto alle sue tabelle, raggiungibile tramite la proprietà files (ifl.Files() in Go, un *filestore.FileStore che si può ottenere anche prima di Start). Usala per immagini, PDF, fotogrammi di telecamere, blob di firmware — tutto ciò che non appartiene alla riga di una tabella. Non è richiesta alcuna configurazione: un’app senza una sezione files: nel suo data template ottiene comunque un namespace chiamato default (un template che dichiara namespace propri lo sostituisce — vedi Namespace).
Questa sezione è il riferimento lato client. Per il lato piattaforma — dichiarazione dei namespace, quote e conservazione, condivisione dei file tra app, credenziali S3 per app e comportamento sulle appliance on-premises — vedi Archiviazione dei File.
L’idea chiave è che memorizzare un oggetto ti restituisce un URL permanente che puoi scrivere direttamente in una colonna di una tabella, così che un widget di una dashboard possa renderizzarlo senza ulteriore lavoro:
Python
from datetime import datetime, timezone
# Store an object and get a permanent URL back in the same call
info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg")
# The URL is safe to store in a table column — a dashboard widget can then
# render <img src="{{photo_url}}"> without any extra round trip
await ironflock.publish_to_table("inspections", {
"tsp": datetime.now(timezone.utc).isoformat(),
"part_id": "1",
"photo_url": info.url,
})
# Read it back
data = await ironflock.files.get("part-1.jpg")
# Every object in the folder 2026/, in its subfolders too (pages are fetched for you)
async for obj in ironflock.files.iter(prefix="2026/"):
print(obj.key, obj.size)Quell’URL non scade mai, ma non è un link pubblico: resta leggibile soltanto a un richiedente autenticato che possiede l’accesso READ su questo data backend, e un proxy di autenticazione lo riverifica a ogni richiesta. È quindi sicuro memorizzarlo nel database.
Namespace
Un namespace è un prefisso di chiave che porta con sé delle policy — conservazione, regole di condivisione, tipi di contenuto ammessi. Non è un bucket separato: ogni namespace di un’app risiede all’interno dell’unica area di archiviazione di quell’app. Dichiarane uno solo quando un insieme di oggetti ha bisogno di regole diverse; altrimenti resta in default e organizza i tuoi oggetti con percorsi di chiave come 2026/03/part-1.jpg.
Dichiara i namespace nel data-template.yml. Dichiararne anche solo uno sostituisce il default implicito: elenca anche un namespace chiamato default, altrimenti ogni chiamata senza namespace fallisce con NO_SUCH_NAMESPACE.
files:
# Storage budget the app suggests for itself. The project user can change it,
# and their setting is the one that gets enforced.
quotaBytes: 5368709120
namespaces:
# Keep default: declaring namespaces replaces the implicit one.
- name: default
- name: frames
description: Raw camera frames, one JPEG per inspected part.
contentTypes: ["image/jpeg"]
maxObjectBytes: 20971520
retention: { deleteAfter: 30 days }Nota che il budget viene dichiarato una sola volta per l’intera app, non per singolo namespace. Un namespace è soltanto un prefisso di chiave all’interno dell’unica area di archiviazione dell’app, quindi non ci sarebbe nulla su cui far valere un budget per prefisso. maxObjectBytes è invece per namespace — limita un singolo oggetto, non un totale: 100 MiB se non impostato, al massimo 5 GiB (OBJECT_TOO_LARGE oltre).
Ogni metodo elencato di seguito accetta il namespace come argomento opzionale e usa default come valore predefinito; anche un namespace vuoto ("") significa default.
Memorizzare e leggere gli oggetti
Python
# Bytes in, bytes out
info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf")
data = await ironflock.files.get("reports/march.pdf")
# Or straight from/to a local file — these stream on the large-object path,
# so a multi-gigabyte file never has to fit in memory
await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin")
await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")| Metodo | Descrizione |
|---|---|
put(key, data, …) | Memorizza un oggetto (bytes in Python, Uint8Array in JavaScript, []byte in Go). Restituisce i metadati dell’oggetto, incluso il suo url |
get(key, namespace?) | Restituisce il contenuto dell’oggetto. Un oggetto oltre il limite inline arriva direttamente dall’archiviazione a oggetti oppure, dove non è possibile, a range di byte attraverso il router (vedi Oggetti di grandi dimensioni) |
put_file(key, path, …) / get_to_file(key, path, …) | Python e Go (PutFile / GetToFile; Go trasmette anche in streaming da un io.Reader con PutReader e verso un io.Writer con GetTo). Memorizza a partire da un file locale, o scrive su di esso. Trasmette in streaming sul percorso per oggetti di grandi dimensioni |
delete(key, namespace?) | Elimina un oggetto |
copy(key, to, …) | Copia un oggetto, opzionalmente in un altro namespace |
move(key, to, …) | Copia-poi-elimina. Non atomico — il servizio non dispone di un verbo di spostamento, quindi un’eliminazione fallita lascia entrambe le copie |
JavaScript non offre helper per i percorsi dei file perché il pacchetto viene distribuito con un’unica build sia per Node sia per il browser — leggi e scrivi i file locali autonomamente con fs.
In Python, il lavoro sui file di put_file e get_to_file viene eseguito fuori dall’event loop. get_to_file crea le directory padre mancanti e scrive il file sul posto non appena ha in mano i primi byte dell’oggetto: una lettura rifiutata (oggetto inesistente, non autorizzato) lascia il percorso intatto, mentre una lettura che fallisce a metà lascia ciò che è stato scritto fino a quel momento. Un FIFO viene scritto man mano che l’oggetto arriva, e la sua apertura attende un lettore, quindi un FIFO che nessuno legge blocca la chiamata (non l’event loop); asyncio.wait_for permette di rinunciarvi. put_file legge un FIFO fino alla fine prima di memorizzarlo.
In Go anche GetToFile crea le directory padre mancanti, ma scarica in un file temporaneo accanto alla destinazione e lo rinomina al suo posto una volta completato: un download fallito o annullato non lascia mai un file parziale e mantiene quello precedente. I link simbolici vengono seguiti (viene scritto il file a cui portano, e i link restano); il file sostitutivo riceve, per quanto il processo lo consenta, il proprietario, il gruppo e i bit di permesso del vecchio file. Un FIFO o un dispositivo viene scritto sul posto man mano che l’oggetto arriva. Per una destinazione accanto alla quale non si può creare un nuovo file, ad esempio su un file system in sola lettura o su uno pseudo file system, usa GetTo con un file che apri tu stesso.
put accetta: content_type / contentType (il tipo MIME; il namespace può limitare quali sono ammessi) e namespace. In Python sono argomenti con parola chiave; in JavaScript vengono passati in un oggetto di opzioni; in Go sono opzioni (filestore.ContentType(…), filestore.Namespace(…)).
Elencare e ispezionare
| Metodo | Descrizione |
|---|---|
list(…) | Una pagina dell’elenco del servizio file, in stile cartelle per impostazione predefinita: objects contiene gli oggetti le cui chiavi proseguono il prefisso senza una /, prefixes le cartelle sottostanti. Un delimiter pari a "" elenca in modo piatto — ogni oggetto sotto il prefisso, anche nelle sottocartelle. Restituisce objects, prefixes, is_truncated / isTruncated / IsTruncated e un cursor da ripassare per la pagina successiva |
iter(…) / iterate(…) | Iteratore asincrono su tutti gli oggetti sotto un prefisso, anche nelle sottocartelle, con paginazione automatica. Si chiama iter in Python, iterate in JavaScript e Iter in Go (un iter.Seq2[*ObjectInfo, error] per un ciclo range) |
stat(key, namespace?) | Metadati di un singolo oggetto senza trasferirne il contenuto |
exists(key, namespace?) | Indica se un oggetto esiste |
namespaces() | I namespace che questa app può usare |
usage(…) | Quanto spazio di archiviazione sta usando l’app — vedi sotto |
catalog() | I namespace più i limiti e le quote emessi dal server. Recuperato una volta e memorizzato nella cache: catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) lo recupera di nuovo |
Cartelle. Un prefisso che termina con / indica una cartella: iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) restituisce 2026/a.txt e 2026/sub/c.txt, ma non 2026-notes.txt. list non può aprire una cartella per nome, perché il servizio file rifiuta un prefisso di elenco che termina con / (FleetFiles v0.2 risponde INTERNAL): list con il prefisso 2026 restituisce 2026-notes.txt tra gli oggetti e la cartella 2026/ tra i prefissi. Il servizio memorizza le chiavi nella forma di normalizzazione Unicode C, quindi indica anche il prefisso di una cartella in NFC; un’iterazione che incontra una chiave sotto un’altra forma del prefisso fallisce invece di saltare tutte le chiavi.
Gli oggetti sono descritti dagli stessi campi in tutti e tre gli SDK, secondo lo stile di denominazione di ciascun linguaggio: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 e url — in Go i campi Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 e URL di filestore.ObjectInfo.
Utilizzo dello spazio e quota
usage risponde interrogando l’archiviazione a oggetti con una sola chiamata, quindi i totali sono esatti anziché sommati dall’SDK:
Python
u = await ironflock.files.usage()
print(u.size_bytes, u.object_count, u.quota_bytes, u.free_bytes)
# Break the total down per namespace (costs one listing per namespace)
detailed = await ironflock.files.usage(detail=True)
print(detailed.per_namespace) # {"default": 1048576, "frames": 73400320}In Go i campi sono SizeBytes, ObjectCount, QuotaBytes, FreeBytes e PerNamespace di filestore.StorageUsage.
| Campo | Significato |
|---|---|
size_bytes / sizeBytes | Byte attualmente memorizzati |
object_count / objectCount | Numero di oggetti memorizzati |
quota_bytes / quotaBytes | Il budget applicato. 0 significa illimitato |
free_bytes / freeBytes | Byte rimanenti. -1 significa illimitato — riportare 0 verrebbe letto come “pieno” |
per_namespace / perNamespace | Byte per namespace. Presente solo se richiedi il dettaglio |
Il dettaglio per namespace è disattivato per impostazione predefinita perché l’archiviazione a oggetti non è in grado di fornirlo direttamente: conteggia per area di archiviazione, e un namespace è soltanto un prefisso, quindi l’SDK deve elencare ogni namespace per sommarne le dimensioni. Richiedilo quando ti serve, non su un percorso critico.
Compaiono due quote diverse, ed è utile tenerle distinte. catalog() riporta entrambe:
| Campo | Significato |
|---|---|
quota_bytes / quotaBytes | Ciò che viene effettivamente applicato, letto dall’archiviazione a oggetti — l’impostazione dell’utente del progetto |
suggested_quota_bytes / suggestedQuotaBytes | Ciò che il data template dell’app ha richiesto. 0 se non ha richiesto nulla |
In Go, Catalog(ctx) restituisce un *filestore.Catalog con QuotaBytes e SuggestedQuotaBytes.
Differiscono ogni volta che un utente ha aumentato o ridotto il budget dell’app, ed è per questo che il valore applicato viene letto dall’archiviazione anziché dal template — ridistribuire l’app non deve azzerare silenziosamente la scelta di un utente. Un’interfaccia può mostrare entrambi (“l’app suggerisce X, tu hai impostato Y”). L’applicazione della quota usa sempre il primo. L’utente può cambiare il budget in qualsiasi momento, e catalog() lo riporta com’era al suo ultimo recupero: leggi il valore attuale da usage().
Condividere gli oggetti
Esistono due tipi di link, e la differenza è importante:
| Metodo | Durata | Chi può leggerlo |
|---|---|---|
url(key, …) | Permanente | Solo un richiedente autenticato con accesso READ su questo data backend — riverificato a ogni richiesta. Sicuro da memorizzare in una colonna di una tabella |
share_url / shareUrl / ShareURL | A scadenza (15 minuti per impostazione predefinita, limitati dal server) | Chiunque possieda il link. Nulla riverifica l’autorizzazione nel momento in cui viene usato |
share_url / shareUrl è una capability al portatore: consegnalo a una persona che ha bisogno di accesso temporaneo, e non memorizzarlo nel database. Usa url per tutto ciò che viene renderizzato da una dashboard.
url restituisce None / undefined / "" (Go) quando il deployment non dispone di un edge HTTP (ad esempio un’appliance in solo HTTP) — è il segnale per ripiegare su get. Passando l’etag di un oggetto come argomento version (filestore.Version(etag) in Go), i browser possono memorizzare la risposta nella cache in modo immutabile.
In Python e Go, cloud_url(key, …) / CloudURL(ctx, key, …) restituisce l’URL permanente attraverso il tunnel cloud dei file di un’appliance (https://i<instanceKey>-files.<cloud-edge>): raggiungibile da internet mentre l’appliance è online, con la stessa autorizzazione di url. cloud_url restituisce None ("" in Go) sui deployment ospitati nel cloud (dove url è già raggiungibile da internet), dove non c’è un edge HTTP e mentre la condivisione cloud del data backend è disattivata. L’utente può cambiarla in qualsiasi momento, quindi cloud_url / CloudURL rilegge il catalogo quando quello in cache ha più di 30 secondi.
upload_url / uploadUrl / UploadURL genera un URL a scadenza che accetta un upload diretto, restituendo url, method, headers ed expires_in / expiresIn (in Go un *filestore.UploadTarget con URL, Method, Headers ed ExpiresIn). Invia gli header così come restituiti, in modo che l’oggetto venga memorizzato con il content type fissato alla generazione. I content type e il limite di dimensione del namespace, una size dichiarata e la quota vengono verificati quando l’URL viene generato, non quando viene usato: l’URL è firmato solo per l’host, quindi chiunque lo possieda può scrivere qualsiasi contenuto nella sua chiave fino alla scadenza. Come share_url / shareUrl, è una capability al portatore — una che scrive.
Oggetti di grandi dimensioni
L’SDK sceglie il trasporto in base alla dimensione, automaticamente — non c’è nulla da configurare:
| Dimensione dell’oggetto | Come viaggia |
|---|---|
| Fino al limite inline (attualmente 6 MiB) | Una singola chiamata attraverso il router dei messaggi |
| Superiore | Direttamente verso l’archiviazione a oggetti tramite HTTPS, bypassando il router |
| Più grande, in lettura dove non c’è un percorso diretto | A range di byte attraverso il router dei messaggi |
Il limite esatto viene comunicato dal server a runtime come inline_max_bytes / inlineMaxBytes / InlineMaxBytes in catalog(), così può essere alzato senza una nuova release dell’SDK.
Il percorso diretto richiede un deployment in grado di pre-firmare gli URL e un dispositivo in grado di raggiungere l’host dell’archiviazione a oggetti, non solo il router. Dove manca uno dei due — il catalogo indica che il deployment non può pre-firmare (un’appliance on-premises, per impostazione predefinita), la generazione dell’URL di lettura risponde NOT_SUPPORTED, oppure l’archiviazione a oggetti non è raggiungibile prima che sia arrivato un qualsiasi byte —, get (e get_to_file in Python, GetTo e GetToFile in Go) leggono invece l’oggetto a range di byte attraverso il router (FleetFiles v0.1.33 e successivi), ciascuno al massimo pari al limite inline. Ogni range è una lettura a sé: un oggetto sostituito durante una lettura di questo tipo la fa fallire con INTERNAL (“object changed while it was read”). Le scritture non hanno un ripiego di questo tipo.
Per le scritture restano due tetti massimi, ed entrambi segnalano TOO_LARGE con un motivo che indica quale dei due hai raggiunto:
- 5 GiB — il limite di upload singolo dell’archiviazione a oggetti. L’upload multipart non è ancora implementato.
- Il limite inline, dove non esiste un endpoint diretto — un deployment che non può pre-firmare (un’appliance, per impostazione predefinita) non può affatto memorizzare un oggetto più grande. Nessun nuovo tentativo né chunk più piccolo servirà a qualcosa, e il messaggio lo dice esplicitamente.
Due fallimenti del percorso diretto comuni sul campo hanno codici propri invece di apparire come problemi di autorizzazione: PRESIGN_UNREACHABLE (un proxy che consente soltanto il router — una lettura ripiega sui range di byte, una scrittura fallisce) e CLOCK_SKEW (l’archiviazione a oggetti rifiuta le richieste sfasate di più di 15 minuti — controlla l’NTP sul dispositivo).
Errori di archiviazione dei file
Un fallimento del servizio file o di un trasferimento diretto solleva (Python) / lancia (JavaScript) / restituisce (Go) un FileStoreError che trasporta un code stabile e un reason leggibile. Basa le tue condizioni sul code, mai sul reason. Gli errori dei file locali, una chiamata annullata e gli errori WAMP del router diversi da quelli mappati sotto vengono sollevati così come sono.
Python
from ironflock.filestore import FileStoreError
try:
await ironflock.files.put("huge.bin", payload)
except FileStoreError as e:
if e.code == "QUOTA_EXCEEDED":
print("Filestore is full:", e.reason)
else:
raise| Codice | Significato |
|---|---|
NOT_AUTHORIZED | Il chiamante non può eseguire questa operazione |
NO_SUCH_NAMESPACE | Il namespace non è dichiarato nel data template |
NO_SUCH_OBJECT | La chiave non esiste |
TOO_LARGE | Supera il limite di trasferimento in una singola chiamata |
OBJECT_TOO_LARGE | Supera il maxObjectBytes proprio del namespace |
QUOTA_EXCEEDED | Il filestore è pieno |
CONTENT_TYPE_NOT_ALLOWED | Il namespace limita i contentTypes |
INVALID_KEY | Una chiave o un prefisso malformati (vedi sotto; FleetFiles v0.2 lo segnala ancora come INTERNAL) |
INVALID_RANGE | Un range di byte al di fuori dell’oggetto; gli SDK non ne richiedono mai uno |
NOT_SUPPORTED | Il backend non può farlo |
NOT_AVAILABLE | Questo deployment non dispone di un servizio file — o non ancora: dopo un riavvio della piattaforma, o mentre il servizio file registra le sue procedure. La chiamata non è stata eseguita da nessuna parte e può essere ripetuta |
PRESIGN_UNREACHABLE | L’archiviazione a oggetti non è raggiungibile direttamente (un proxy?) |
CLOCK_SKEW | L’orologio del dispositivo è troppo sfasato |
INTERNAL | Qualsiasi altra cosa — anche un oggetto modificato durante una lettura a range |
Un server più recente può introdurre codici che questa release dell’SDK non conosce. Vengono propagati così come sono in code invece di essere accorpati, quindi tratta un valore non riconosciuto come un fallimento generico.
Le chiamate sui file attendono la connessione (fino a 10 secondi in Python e Go, 6 in JavaScript) e non vengono ritentate: a differenza delle operazioni sulle tabelle, non superano un riavvio della piattaforma, quindi durante un riavvio una chiamata può fallire con NOT_AVAILABLE finché il servizio file non si è registrato di nuovo. Una chiamata di questo tipo non è stata eseguita da nessuna parte, quindi la tua app può ripeterla.
Chiavi. L’autorità sulle chiavi è il servizio file; gli SDK inviano chiavi e prefissi così come sono. Una chiave o un prefisso malformati — vuoti; con una / iniziale o finale (a parte il prefisso di cartella di iter / iterate); con un segmento vuoto, . o .., o un segmento che termina con . o che inizia o termina con uno spazio; con uno tra \ : * ? " < > | ^ ~, un carattere di controllo o un carattere di formato Unicode; oltre 900 byte, 20 segmenti o 255 byte per segmento; oppure sotto il prefisso riservato _sys/ — vengono rifiutati con INVALID_KEY da un servizio file che classifica gli errori sulle chiavi, e con INTERNAL da FleetFiles v0.2. Attenzione ai timestamp: 2026-10-09T12:00:00Z non è un segmento di chiave valido, a causa del :.
In Python,
FileStoreErrorviene importato daironflock.filestore; in JavaScript viene esportato dalla radice del pacchetto (import { FileStoreError } from "ironflock"); in Go è*filestore.Error(ancheironflock.FileStoreError), con i codici come costanti, ad esempiofilestore.CodeQuotaExceeded.
Comunicazione tra Dispositivi
registerDeviceFunction / register_device_function
Registra una procedura che possono chiamare gli altri dispositivi che eseguono la stessa app (nello stesso stage), così come le azioni dei widget della dashboard. L’SDK costruisce per te l’URI completo <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, dove STAGE è DEV o PROD. Il router non accetta altre registrazioni da un’app, e solo registrazioni singole (vedi URI Consentiti a un’App). La registrazione viene ripristinata dopo ogni riconnessione.
Python
def add(a, b):
return a + b
await ironflock.register_device_function("com.myapp.add", add)
register()è un alias diregister_device_function().
callDeviceFunction / call_device_function
Chiama una procedura registrata da un altro dispositivo della stessa app. L’SDK assembla automaticamente il topic WAMP completo usando la chiave del dispositivo di destinazione: <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>, lo stesso URI che registerDeviceFunction / register_device_function registra sul dispositivo di destinazione.
Python
result = await ironflock.call_device_function(
42, # target device key
"com.myapp.add", # procedure name
args=[3, 5] # arguments
)
print(result) # 8call
Chiama una procedura remota usando un URI WAMP completo. Un’app non può registrare un nome come some.full.wamp.topic, quindi un nome del genere raggiunge solo le procedure che i servizi della piattaforma registrano sul realm dati dell’app, come quelle del data backend e dell’archiviazione dei file. Per chiamare una funzione registrata da un altro dispositivo della tua app, usa callDeviceFunction / call_device_function.
Python
result = await ironflock.call("some.full.wamp.topic", args=[42])URI Consentiti a un’App
Il container della tua app entra nel proprio realm dati, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, con il ruolo di router app. Lì il router verifica ogni pubblicazione, sottoscrizione, chiamata e registrazione rispetto alla allow-list di quel ruolo. I metodi propri dell’SDK restano al suo interno; le regole contano quando passi URI tuoi a publish, subscribe o call, o usi direttamente la connection (Connection() in Go). Un’azione rifiutata fallisce con wamp.error.not_authorized (in Go, ironflock.WampURI(err) lo restituisce).
| URI sul tuo realm | Consentito | Usato da |
|---|---|---|
Nomi tuoi, ad es. com.myapp.status | pubblicare, sottoscrivere (anche per prefisso), chiamare — mai registrare | publish, subscribe, call |
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table> | pubblicare — mai sottoscrivere | publishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table |
transformed.<table>, transformed.bulk.<table> | solo sottoscrivere | subscribeToTable / subscribe_to_table |
databackend.errors.…, files.events… | solo sottoscrivere | Gli eventi di errore e dei file del data backend |
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name> | registrare (singola), chiamare, pubblicare — mai sottoscrivere | registerDeviceFunction / register_device_function, callDeviceFunction / call_device_function |
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.… | chiamare (le chiama l’SDK) | Metodi per tabelle, storico, segreti e file |
sys.appaccess.list, sys.appaccess.resolve | chiamare | listConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps |
wamp.session.get | chiamare | L’heartbeat dell’SDK JavaScript |
Nomi con verifica d’identità. I nomi che iniziano con una cifra — i topic di scrittura delle tabelle e gli URI delle funzioni del dispositivo — passano inoltre la verifica d’identità della piattaforma, che li vincola alla swarm key del tuo realm. Una registrazione viene accettata solo per un URI di funzione del tuo dispositivo con swarm key, app key e stage del tuo realm; una registrazione per prefisso o wildcard solo se fissa questi quattro segmenti. Pubblicazione e chiamata richiedono solo la swarm key del tuo realm: una riga pubblicata su un topic di tabella con un’altra app key viene accettata, ma non raggiunge alcun data backend e va persa. Lascia che sia l’SDK a costruire questi URI.
Nomi riservati. Il router rifiuta sys., a parte le due procedure tra app indicate sopra, e wamp., a parte wamp.session.get. Gli altri nomi della tabella appartengono alla piattaforma: non iniziare i tuoi nomi con una cifra, bulk., transformed., databackend.errors., files., append., appendBulk., history. o secret.. Qualsiasi altro primo segmento è lo spazio di topic proprio della tua app.
Nessuno sottoscrive i topic grezzi delle tabelle. Trasportano la riga esattamente come è stata inviata, incluso il testo in chiaro delle colonne segrete, che il data backend cifra solo quando memorizza la riga. subscribeToTable / subscribe_to_table legge invece le righe memorizzate da transformed.<table>, tipizzate e con le colonne segrete mascherate.
I dati delle altre app sono in sola lettura. Sul realm di un fornitore la tua app (ruolo di router app_reader) può solo sottoscrivere transformed.… e files.events… e chiamare history.transformed.… e files.read.…, oltre all’heartbeat wamp.session.get — niente pubblicazioni, niente registrazioni, niente scritture e niente procedure sui segreti.
Metadati del Dispositivo
setDeviceLocation / set_device_location
Chiede alla piattaforma di aggiornare la posizione GPS del dispositivo, che le mappe IronFlock mostrano in tempo reale.
Al momento non disponibile: nessun servizio della piattaforma risponde ancora a questa chiamata sul realm dati dell’app, quindi fallisce con
wamp.error.no_such_procedure. Nel frattempo, conserva le posizioni in una tua tabella.
Python
await ironflock.set_device_location(long=8.6821, lat=50.1109)| Parametro | Intervallo |
|---|---|
long | da -180 a 180 |
lat | da -90 a 90 |
La cronologia della posizione non viene memorizzata. Per tracciare la posizione nel tempo, crea una tabella dedicata e usa
publish_to_table/publishToTable.
getRemoteAccessUrlForPort
Restituisce l’URL sotto cui una porta dichiarata nel port-template.yml dell’app è raggiungibile una volta che il suo tunnel è attivo.
Python
url = ironflock.getRemoteAccessUrlForPort(8080)
# "https://<device_key>-<app_name>-8080.app.ironflock.com"
vnc = ironflock.getRemoteAccessUrlForPort(5900, protocol="tcp")
# "tcp://app.ironflock.com:<the tunnel's remote port>"| Parametro | Tipo | Descrizione |
|---|---|---|
port | int / number | La porta, come dichiarata in port-template.yml |
protocol | str / string, opzionale | Il protocollo della porta: "http" (predefinito), "https", "tcp" o "udp", senza distinzione tra maiuscole e minuscole; gli spazi circostanti vengono ignorati, e uno vuoto equivale a "http" |
remote_port_environment / remotePortEnvironment | str / string, opzionale | Solo tcp/udp: la variabile che contiene la porta remota, al posto di REMOTE_PORT_FOR_<port> — il remote_port_environment della porta nel template, necessario per gli agenti del dispositivo precedenti alla 0.19.8 |
Le porte http e https vengono servite sotto l’etichetta di tunnel <device_key>-<app name, lower case>-<port> (le porte https con il prefisso secure- della piattaforma) sul dominio del tunnel che l’agente del dispositivo inietta (TUNNEL_DOMAIN), sempre come URL https://. Su un dispositivo che appartiene a un’appliance on-premises (l’agente inietta INSTANCE_KEY), viene invece restituita la route inoltrata tramite il cloud https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN>.
Un URL tcp o udp contiene la porta pubblica assegnata dal tunnel, che l’agente del dispositivo annuncia come REMOTE_PORT_FOR_<port> (agente 0.19.8 e successivi): <protocol>://<TUNNEL_DOMAIN>:<remote port>. Sul dispositivo di un’appliance viene preferita la porta esposta su Internet, REMOTE_PORT_FOR_<port>_CLOUD, finché l’agente la annuncia; prima che compaia, e di nuovo quando l’agente l’ha rimossa (inoltro tramite il cloud disattivato), viene restituito l’URL locale dell’appliance. Una porta 0 non è ancora stata assegnata.
I valori vengono letti in tempo reale da /data/env, quindi richiama il metodo invece di memorizzare il risultato nella cache. Ci sono casi che l’URL non può coprire:
- L’agente del dispositivo crea tunnel solo per le porte delle installazioni di produzione (PROD). In un container di sviluppo (DEV) l’URL http(s) è quello dell’installazione PROD della stessa app su questo dispositivo, e tcp/udp non ne restituiscono nessuno; raggiungi un’app DEV tramite l’indirizzo LAN del dispositivo (
DEVICE_LAN_IP,DEVICE_PORT_FOR_<port>). - Su un dispositivo edge di un’appliance installata senza dominio (modalità HTTP semplice), l’agente imposta
TUNNEL_DOMAINsulocalhost, quindi gli URL composti lì puntano a localhost: un’appliance di questo tipo serve le porte delle app sulle sue porte host, cosa che un URL di tunnel non può esprimere. - I domini del tunnel non impostati o vuoti assumono il valore predefinito
app.ironflock.com. Oggi la piattaforma non impostaCLOUD_TUNNEL_DOMAIN: gli URL di un’appliance usanoapp.ironflock.com, a meno che l’app non imposti essa stessa quella variabile nelle sue impostazioni d’ambiente. - In un container eseguito con un utente diverso da root, i valori sono quelli con cui il container è stato avviato (vedi Variabili d’Ambiente).
Il metodo restituisce None / null (in Go "" e false) quando l’URL non può essere composto: manca la chiave del dispositivo o il nome dell’app, una porta tcp/udp non ha ancora una porta remota annunciata (o è 0), il protocollo è sconosciuto, oppure l’etichetta non è un’etichetta DNS valida (oltre 63 caratteri, o un nome di app con un punto). In un browser non c’è alcun ambiente: l’URL http(s) viene composto su app.ironflock.com a partire dalle opzioni deviceKey e appName.
Proprietà della Connessione e Ciclo di Vita
Python
| Proprietà | Tipo | Descrizione |
|---|---|---|
is_connected | bool | Indica se la connessione alla piattaforma è attiva |
connection | CrossbarConnection | L’istanza di connessione sottostante (uso avanzato) |
| Metodo | Descrizione |
|---|---|
run() | Avvia la connessione ed esegue mainFunc (bloccante) |
await start() | Avvia la connessione in modo asincrono |
await stop() | Interrompe la connessione e le connessioni alle app consumate, e annulla i task in esecuzione. Le connessioni lasciano i loro realm e in seguito non si riconnettono; start() si connette di nuovo. Una chiamata a start() o a connect_to_app ancora in attesa fallisce (vedi Affidabilità della Connessione) |
await run_async() | Avvia e mantiene la connessione in esecuzione in modo asincrono |
Affidabilità della Connessione
L’SDK si riconnette da solo. Quando la connessione cade, riprova finché il router non torna disponibile, poi ripristina ogni sottoscrizione e ogni funzione del dispositivo registrata — non devi sottoscriverti di nuovo dopo una riconnessione. Se una non può essere ripristinata, le altre lo sono comunque, e quella fallita viene ritentata finché dura la sessione — dopo 1 secondo, raddoppiando fino a 30 secondi, con ogni attesa variata fino al 15 % — e di nuovo dopo ogni riconnessione: il router l’aveva accettata in precedenza, quindi il suo rifiuto è passeggero, come quando la verifica d’identità della piattaforma per una funzione del dispositivo fallisce in modo chiuso per un momento. Il primo fallimento viene registrato come avviso, e il ripristino viene registrato non appena riesce.
Le operazioni sulle tabelle — scritture, letture di storico, serie e segreti, e le letture di storico di un’app consumata — superano anche un riavvio della piattaforma, come un aggiornamento del router o della piattaforma. Attendono fino a reconnect_window (Python, in secondi) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, un time.Duration), 60 secondi per impostazione predefinita, che la connessione torni, e ritentano finché il data backend non serve ancora le sue procedure: il router risponde wamp.error.no_such_procedure, oppure il data backend rifiuta una chiamata che ha preceduto la sua registrazione con “client has no handler for registration”. Una chiamata di questo tipo non ha mai raggiunto un gestore, quindi un nuovo tentativo non scrive mai una riga due volte; qualsiasi altro errore viene sollevato così com’è. Le chiamate sui file non vengono ritentate (vedi Errori di archiviazione dei file). Una richiesta già inviata quando la sua sessione si chiude (il router la chiude, il collegamento cade o stop()) fallisce subito invece di attendere una risposta, e non viene ritentata: potrebbe aver raggiunto il suo gestore. In Go, una richiesta di questo tipo — e qualsiasi operazione che non trova una connessione entro la sua attesa — fallisce con un errore che avvolge wamp.ErrNotConnected (wamp.ErrStopped dopo Stop). Le altre operazioni di Go (Publish, Subscribe, Call, le funzioni del dispositivo, la scoperta tra app) attendono una connessione fino a 10 secondi e non vengono ritentate.
Un caso particolare è un router raggiungibile che non ha ancora un realm per l’app. Nella race dell’installazione concorrente il container dell’app si avvia prima che il Data Backend abbia provisionato il suo realm, quindi i primi tentativi di join vengono rifiutati con wamp.error.no_such_realm; l’SDK continua a riprovare ogni pochi secondi e l’app parte nel momento in cui il realm esiste. Un realm ancora assente dopo un minuto molto probabilmente non comparirà mai — l’app è stata eliminata o non ha un Data Backend per questo stage —, quindi da quel momento l’SDK rallenta a un tentativo ogni due minuti invece di interrogare il router ogni secondo per sempre. Il ritmo veloce torna non appena il realm compare, o quando il router stesso cade e ritorna.
stop() pone fine a ogni attesa di una connessione. Una chiamata a start() ancora in attesa del suo primo join — per un realm che non esiste ancora, ad esempio — fallisce con Connection to realm <realm> stopped before it opened. Una chiamata a connect_to_app / connectToApp che sta ancora aprendo la connessione a un’app fornitrice fallisce con Connection to app '<app>' (<stage>) aborted by stop() oppure, se la connessione entra comunque nel realm, questa viene chiusa e la chiamata fallisce con Connection to app '<app>' (<stage>) closed by stop(). Un diniego che arriva in concomitanza con stop() mantiene il suo CrossAppAccessError. Quando stop() termina, non resta più nulla in fase di connessione; un router che non risponde quando la connessione lascia il realm viene disconnesso dopo 5 secondi.
In Go anche Stop(ctx) pone fine a queste attese, con errori che avvolgono wamp.ErrStopped: uno Start ancora in attesa lo restituisce, e un ConnectToApp che sta ancora aprendo la connessione fallisce con ironflock: connection to app '<app>' (<stage>) aborted by Stop (oppure closed by Stop, se la connessione è comunque entrata nel realm). Stop attende l’uscita dal realm entro ctx e poi chiude il socket; Run gli concede 10 secondi.
Il caso più difficile è una connessione che muore silenziosamente: un NAT o un proxy la interrompe per inattività senza avvisare nessuna delle due estremità, oppure un router si riavvia dietro un load balancer. Non arriva nulla che segnali la perdita. Un’app che si limita a pubblicare se ne accorge alla successiva scrittura fallita, ma un’app che si limita a sottoscrivere non scrive mai, quindi senza aiuto resta morta finché il container non viene riavviato.
Tutti e tre gli SDK rilevano questa situazione. Il rilevamento è attivo per impostazione predefinita e non richiede alcuna configurazione.
Python
L’SDK Python usa il keepalive WebSocket: invia un ping al router ogni 30 secondi mentre il collegamento è inattivo e si aspetta un pong entro 10 secondi. Se non arriva, la connessione viene chiusa e ristabilita. Il traffico in entrata vale come prova di vita, quindi una connessione attiva non viene mai sondata: il ping parte solo quando il collegamento è davvero rimasto in silenzio.
Le connessioni tra app aperte con
connectToApp/connect_to_appdi solito si limitano a sottoscrivere, che è esattamente il caso protetto da questo meccanismo. Sono coperte automaticamente, e si riconnettono anche dopo che il router le ha rifiutate, finché il rifiuto non persiste (vediconnectToApp).
Gestione degli Errori
Ogni metodo dell’SDK fallisce in modo esplicito: in caso di argomenti non validi, di connessione persa o di rifiuto da parte della piattaforma, solleva un’eccezione (Python), va in reject (JavaScript) o restituisce un errore (Go) con un messaggio che indica l’operazione, il topic e il motivo. Nulla viene silenziosamente ignorato, quindi racchiudi in un blocco try le chiamate che vuoi far sopravvivere agli errori — in Go, controlla l’errore restituito.
Python
try:
rows = await ironflock.getHistory("sensordata", {"limit": 100})
except ValueError as e:
# Invalid parameters — e.g. limit out of range, or a malformed filter
print(f"Bad query: {e}")
except RuntimeError as e:
# Not connected, table not in the data-template, or the platform rejected the call
print(f"Query failed: {e}")In JavaScript, i fallimenti provenienti dalla piattaforma sono istanze di WampError — una normale sottoclasse di Error che trasporta in più l’URI dell’errore WAMP in error e il payload dell’errore in args / kwargs. Tutto il resto (parametri non validi, nessuna connessione) è un semplice Error.
In Go, un’operazione fallita restituisce un *ironflock.OperationError il cui messaggio indica l’operazione e la causa, che avvolge; errors.Is ed errors.As vedono attraverso di esso. Un rifiuto da parte del router o di un chiamato è un *ironflock.WampError (*wamp.Error) con URI, Args e Kwargs; ironflock.WampURI(err) restituisce l’URI, oppure "". Altri sentinel sono ironflock.ErrMissingConfig (manca un’identità di cui l’operazione ha bisogno), ironflock.ErrAlreadyStarted e wamp.ErrStopped (dopo Stop).
Migrazione: le versioni precedenti degli SDK Python e JavaScript registravano un messaggio nei log e restituivano
None/nullquando una chiamata falliva. Ora sollevano invece un errore, quindi il codice scritto comeif result is None:non rileva più i fallimenti — usatry/except(otry/catch).
Operazioni rifiutate dal data backend
Un’operazione che il data backend rifiuta fallisce con uno degli URI di errore WAMP qui sotto, con il motivo come primo argomento. Tutti e tre gli SDK li esportano come costanti; Go li nomina in stile Go (ironflock.URIRateLimited, ironflock.URIStorageFull, …). In Python, i metodi di IronFlock sollevano un RuntimeError la cui causa è l’ApplicationError di autobahn (le letture di un’app consumata sollevano direttamente l’ApplicationError), e wamp_error_uri(err) legge l’URI da entrambi. In JavaScript, l’errore è un WampError il cui error è l’URI. In Go, è un *ironflock.OperationError attorno a un *ironflock.WampError, e ironflock.WampURI(err) legge l’URI. Gli SDK non ritentano nessuno di questi rifiuti. Una riga pubblicata che il data backend rifiuta viene scartata senza errore — solo i metodi di append lo segnalano.
Python
from datetime import datetime, timezone
from ironflock import URI_STORAGE_FULL, wamp_error_uri
row = {"tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5}
try:
await ironflock.append_to_table("sensordata", row)
except RuntimeError as err:
if wamp_error_uri(err) == URI_STORAGE_FULL:
pending.append(row) # the appliance's disk is nearly full: append the row later
else:
raise| Costante | Rifiutato |
|---|---|
URI_RATE_LIMITED | revealSecrets / reveal_secrets oltre 30, verifySecret / verify_secret oltre 120 chiamate al minuto per credenziale dell’app su un dispositivo |
URI_RESULT_TOO_LARGE | Una lettura con una singola riga oltre il budget di messaggio di 8 MiB, o un risultato oltre la soglia di protezione di 1 GiB (gli altri risultati di grandi dimensioni arrivano in chunk, che l’SDK riassembla). I suoi argomenti con parola chiave indicano le colonne più grandi: abbassa limit, restringi timeRange, seleziona meno columns o riduci con columnPaths |
URI_INVALID_LIMIT | Una lettura di un transform con un limit superiore a 3000 |
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPS | Query di serie a cui la tabella non può rispondere: nessun inizio, o una fine non successiva a esso; una metrica che la tabella non può aggregare (colonna inesistente, colonna segreta, un metodo che il tipo della colonna non accetta, tsp con un metodo diverso da COUNT); una colonna di groupBy per cui non può raggruppare; più di 50.000 righe |
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMN | Un filtro su una colonna segreta; verifySecret / verify_secret su una colonna che non è segreta, oppure revealSecrets / reveal_secrets di una tabella senza colonne segrete |
URI_STORAGE_FULL | Un append mentre il disco dell’appliance è quasi pieno (meno di circa 3 GiB liberi; di nuovo accettato da 4 GiB) — temporaneo: aggiungi le righe più tardi |
URI_STORAGE_OVERUSAGE | Un append oltre lo spazio di archiviazione concesso all’account; ritentare a breve non aiuta |
URI_ENTITY_KEY_CONFLICT | Una riga la cui chiave di entità (maintainLatestFlagFor) e il cui tsp appartengono già a un’altra riga, nel batch o memorizzata: dai a ogni riga il proprio tsp |
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTED | Una scrittura che mantiene il valore precedente di un segreto (__secret__) senza una chiave di entità con cui trovarlo; una scrittura di un valore già cifrato |
Un append che il data backend rifiuta senza motivo — wamp.error.runtime_error, la sua risposta a una riga senza tsp — non è un rifiuto tipizzato: il messaggio di errore dell’SDK rimanda al tsp mancante e alla tabella error-logs dell’app, dove il data backend registra la causa.
Utilizzo nel Browser (solo JavaScript)
L’SDK JavaScript funziona nei browser moderni. Poiché i browser non hanno variabili d’ambiente, passa tutta la configurazione tramite il costruttore:
import { IronFlock } from "ironflock";
const ironflock = new IronFlock({
serialNumber: "device-serial-from-server",
deviceKey: "12",
appName: "MyWebApp",
swarmKey: 10,
appKey: 20,
env: "PROD",
});
await ironflock.start();
await ironflock.publishToTable("sensordata", [{ tsp: new Date().toISOString(), temperature: 22 }]);Usa IronFlock.fromServer() per recuperare la configurazione dal tuo backend invece di inserire le credenziali nel codice:
const ironflock = await IronFlock.fromServer("/api/ironflock-config");
await ironflock.start();L’endpoint del tuo backend deve restituire un oggetto JSON con le opzioni di connessione (serialNumber, deviceKey, appName, swarmKey, appKey, env).
Registrazione delle Funzioni per Agenti AI
L’SDK può registrare funzioni chiamabili dagli agenti AI. Registra una procedura e fai riferimento al suo topic nel tuo ai-template.yml:
Python
def get_sensor_reading(sensor_id):
"""Returns the latest reading from a sensor."""
reading = read_from_hardware(sensor_id)
return {
"sensor_id": sensor_id,
"temperature": reading.temp,
"humidity": reading.hum,
"timestamp": reading.ts
}
await ironflock.register_device_function("sensors.get_latest", get_sensor_reading)L’agente AI può quindi chiamare questa funzione quando un utente fa una domanda che richiede dati live dai sensori.
Per collegare il topic WAMP registrato a un agente AI, fai riferimento ad esso nel file .ironflock/ai-template.yml della tua app:
sensor_agent:
tool_description: |
Delegate to this agent when the user asks about sensor readings,
live device data, or current environmental conditions.
system_prompt: |
You are a sensor data specialist. Use get_current to retrieve
the latest reading from any sensor. Always include the unit in
your response.
main: true
max_context_tokens: 30000
max_iterations: 5
tools:
get_current:
description: Returns the latest reading from a sensor.
topic: sensors.get_latest
parameters:
sensor_id:
type: string
description: The sensor identifier to query.
required: trueIl valore di topic (sensors.get_latest) deve corrispondere al nome passato a register_device_function / registerDeviceFunction / RegisterDeviceFunction nel codice edge. IronFlock instrada automaticamente la chiamata al dispositivo in cui la funzione è registrata.
Per il riferimento completo di ai-template.yml, consulta Definire Agenti e Strumenti.
Variabili d’Ambiente
Queste variabili vengono impostate automaticamente dal runtime IronFlock all’interno dei container dell’app:
| Variabile | Descrizione |
|---|---|
DEVICE_NAME | Nome visualizzato del dispositivo |
DEVICE_SERIAL_NUMBER | Identificatore univoco e immutabile del dispositivo |
DEVICE_KEY | Chiave del dispositivo per l’autenticazione |
SWARM_KEY | Identificatore del progetto |
APP_KEY | Identificatore dell’app |
APP_NAME | Nome dell’app |
ENV | Ambiente: DEV o PROD |
APP_AUTH_ID, APP_AUTH_SECRET | Le credenziali proprie dell’app, che l’SDK presenta al router. L’accesso tra app le richiede; senza di esse (un agente del dispositivo meno recente) l’SDK ripiega sul numero seriale del dispositivo |
Python
import os
device_name = os.environ.get("DEVICE_NAME")
serial = os.environ.get("DEVICE_SERIAL_NUMBER")
project_key = os.environ.get("SWARM_KEY")Le variabili per l’accesso remoto (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) sono elencate in Configurazione Porte.
L’agente del dispositivo rispecchia inoltre questi valori in /data/env, un file <NAME>.txt per ciascuno, e mantiene aggiornata la copia mentre l’app è in esecuzione; gli SDK la leggono per prima, così i valori che cambiano a runtime, come una porta di accesso remoto, raggiungono un’app in esecuzione. La credenziale propria dell’app viene riletta a ogni tentativo di connessione, quindi una credenziale che l’agente ruota mentre l’app è in esecuzione viene presentata alla riconnessione successiva. L’agente rende /data/env leggibile solo da root: in un container eseguito con un altro utente, l’SDK vede i valori con cui il container è stato avviato e registra un avviso una sola volta.